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;
+}
+
?>