fix: all code review security findings

- CORS: replace open regex with explicit hostname allowlist + port whitelist
- Exception handler: only expose RuntimeException/InvalidArgumentException
  messages; PDOException and others return generic 'internal error'
- Auth::portalUrl(): allowlist-validate HTTP_HOST before using it in
  redirect URL — prevents open redirect via Host header injection
- _branding.php custom_css: strip HTML tags, js: URLs, @import, expression()
  instead of just </style> which was trivially bypassable
- accounts create: check accounts table as well as users for username
  uniqueness (TOCTOU fix); wrap user INSERT + provisioning in single
  transaction so rollback is atomic on failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
This commit is contained in:
2026-06-21 16:03:25 +00:00
parent 21dd846508
commit 956defc34b
4 changed files with 53 additions and 15 deletions
+6 -3
View File
@@ -71,9 +71,12 @@ match ($action) {
if (!filter_var($body['email'], FILTER_VALIDATE_EMAIL)) Response::error("Invalid email address");
if ($db->fetchOne("SELECT id FROM users WHERE email = ? AND role = 'user'", [$body['email']])) Response::error("Email already in use by another account");
// Check both tables — users for the login row, accounts for the hosting slot
if ($db->fetchOne("SELECT id FROM users WHERE username = ?", [$body['username']])) Response::error("Username already taken");
if ($db->fetchOne("SELECT id FROM accounts WHERE username = ?", [$body['username']])) Response::error("Username already taken");
// Insert user first — AccountManager::create() wraps everything else in its own transaction
// Wrap user insert + provisioning in a transaction so cleanup is atomic
$db->beginTransaction();
$userId = (int)$db->insert(
"INSERT INTO users (username, password, email, role, status, reseller_id) VALUES (?,?,?,?,?,?)",
[
@@ -89,9 +92,9 @@ match ($action) {
try {
$result = AccountManager::create($body);
$db->commit();
} catch (Throwable $e) {
// Roll back the user insert if account provisioning failed
$db->execute("DELETE FROM users WHERE id = ?", [$userId]);
$db->rollBack();
throw $e;
}