From 37cf88a06e35b8db82d2aa50d16d427cb18a17de Mon Sep 17 00:00:00 2001 From: janis steiner Date: Fri, 15 May 2026 10:13:23 +0200 Subject: [PATCH] fixing potentiall xss in external domains list --- app-code/account/index.php | 54 ++++++++++++++++--- app-code/api/account/manage_domains.php | 8 ++- .../api/login/confirm_external_redirect.php | 10 ++-- app-code/api/utils/security.php | 28 ++++++++-- 4 files changed, 85 insertions(+), 15 deletions(-) diff --git a/app-code/account/index.php b/app-code/account/index.php index 66f958e..96425de 100644 --- a/app-code/account/index.php +++ b/app-code/account/index.php @@ -818,8 +818,25 @@ function updatePasswordStrength() { data.domains.forEach(d => { const item = document.createElement('div'); item.className = 'list-group-item d-flex justify-content-between align-items-center'; - item.innerHTML = '' + d.domain + '
Approved: ' + d.confirmed_at + '
' + - ''; + + const details = document.createElement('span'); + const domain = document.createElement('strong'); + domain.textContent = d.domain; + const approvedAt = document.createElement('small'); + approvedAt.className = 'text-muted'; + approvedAt.textContent = 'Approved: ' + d.confirmed_at; + details.appendChild(domain); + details.appendChild(document.createElement('br')); + details.appendChild(approvedAt); + + const revokeButton = document.createElement('button'); + revokeButton.type = 'button'; + revokeButton.className = 'btn btn-sm btn-outline-danger'; + revokeButton.textContent = 'Revoke'; + revokeButton.addEventListener('click', () => removeDomain(Number(d.id))); + + item.appendChild(details); + item.appendChild(revokeButton); list.appendChild(item); }); }); @@ -881,9 +898,30 @@ function updatePasswordStrength() { 'sessions_revoked': 'Sessions revoked', }; const label = actionLabels[e.action] || e.action; - item.innerHTML = '
' + label + '' + e.created_at + '
' + - '' + (e.ip ? e.ip + ' · ' : '') + (e.user_agent ? e.user_agent.substring(0, 60) + '...' : '') + '' + - (e.details ? '
' + e.details + '' : ''); + + const header = document.createElement('div'); + header.className = 'd-flex w-100 justify-content-between'; + const action = document.createElement('strong'); + action.textContent = label; + const createdAt = document.createElement('small'); + createdAt.className = 'text-muted'; + createdAt.textContent = e.created_at; + header.appendChild(action); + header.appendChild(createdAt); + + const metadata = document.createElement('small'); + metadata.className = 'text-muted'; + metadata.textContent = (e.ip ? e.ip + ' - ' : '') + (e.user_agent ? e.user_agent.substring(0, 60) + '...' : ''); + + item.appendChild(header); + item.appendChild(metadata); + + if (e.details) { + const details = document.createElement('small'); + details.textContent = e.details; + item.appendChild(document.createElement('br')); + item.appendChild(details); + } list.appendChild(item); }); }); @@ -904,7 +942,11 @@ function updatePasswordStrength() { data.sessions.forEach(s => { const item = document.createElement('div'); item.className = 'list-group-item d-flex justify-content-between align-items-center'; - item.innerHTML = '' + (s.user_agent || 'Unknown device') + ''; + const device = document.createElement('span'); + const deviceName = document.createElement('strong'); + deviceName.textContent = s.user_agent || 'Unknown device'; + device.appendChild(deviceName); + item.appendChild(device); list.appendChild(item); }); }); diff --git a/app-code/api/account/manage_domains.php b/app-code/api/account/manage_domains.php index 5456e55..aa46fa6 100644 --- a/app-code/api/account/manage_domains.php +++ b/app-code/api/account/manage_domains.php @@ -19,6 +19,12 @@ if ($method === 'GET') { $result = mysqli_stmt_get_result($stmt); $domains = []; while ($row = mysqli_fetch_assoc($result)) { + $domain = normalize_redirect_host($row['domain'] ?? ''); + if ($domain === null) { + continue; + } + $row['domain'] = $domain; + $row['id'] = (int) $row['id']; $domains[] = $row; } mysqli_stmt_close($stmt); @@ -45,4 +51,4 @@ if ($method === 'GET') { } else { echo json_encode(['success' => false, 'message' => 'Invalid request method.'], 405); } -?> \ No newline at end of file +?> diff --git a/app-code/api/login/confirm_external_redirect.php b/app-code/api/login/confirm_external_redirect.php index eb62e0a..541b555 100644 --- a/app-code/api/login/confirm_external_redirect.php +++ b/app-code/api/login/confirm_external_redirect.php @@ -8,11 +8,11 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { exit; } -$input = json_decode(file_get_contents('php://input'), true); -$domain = $input['domain'] ?? ''; +$send_to = normalize_redirect_target($_SESSION["end_url"] ?? "/account/"); +$domain = is_external_domain($send_to); -if ($domain === '' || !isset($_SESSION['id'])) { - echo json_encode(['success' => false, 'message' => 'Missing domain or not logged in.']); +if ($domain === null || !isset($_SESSION['id'])) { + echo json_encode(['success' => false, 'message' => 'Missing external domain or not logged in.']); exit; } @@ -26,4 +26,4 @@ mysqli_stmt_bind_param($stmt, 'is', $user_id, $domain); mysqli_stmt_execute($stmt); mysqli_stmt_close($stmt); -echo json_encode(['success' => true]); \ No newline at end of file +echo json_encode(['success' => true]); diff --git a/app-code/api/utils/security.php b/app-code/api/utils/security.php index 7fcc0af..d6fc67b 100644 --- a/app-code/api/utils/security.php +++ b/app-code/api/utils/security.php @@ -282,6 +282,10 @@ function normalize_redirect_target(?string $target): string return '/account/'; } + if (normalize_redirect_host($parts['host']) === null) { + return '/account/'; + } + return $target; } @@ -310,12 +314,11 @@ function is_external_domain(string $url): ?string return null; } - $host = parse_url($url, PHP_URL_HOST); - if ($host === null || $host === '') { + $host = normalize_redirect_host((string) parse_url($url, PHP_URL_HOST)); + if ($host === null) { return null; } - $host = strtolower($host); if ($host === 'auth.jakach.ch' || str_ends_with($host, '.jakach.ch')) { return null; } @@ -323,4 +326,23 @@ function is_external_domain(string $url): ?string return $host; } +function normalize_redirect_host(string $host): ?string +{ + $host = rtrim(strtolower(trim($host)), '.'); + + if ($host === '' || strlen($host) > 253 || preg_match('/[\x00-\x20\x7f<>"\'`]/', $host)) { + return null; + } + + if (filter_var($host, FILTER_VALIDATE_IP)) { + return $host; + } + + if (!filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) { + return null; + } + + return $host; +} + ?>