Compare commits

..

4 Commits

Author SHA1 Message Date
janis 37cf88a06e fixing potentiall xss in external domains list
Deploy / deploy (push) Successful in 28s
2026-05-15 10:13:23 +02:00
janis eb3ffed163 adding http security headers 2026-05-15 10:08:08 +02:00
janis 091d00b5c2 fixing version leakage 2026-05-15 10:06:55 +02:00
janis 10fb66c470 set coockies to secure 2026-05-15 09:59:51 +02:00
6 changed files with 112 additions and 29 deletions
+11
View File
@@ -1,7 +1,18 @@
ServerTokens Prod
ServerSignature Off
TraceEnable Off
<VirtualHost *:80>
ServerName auth.jakach.ch
DocumentRoot /var/www/html
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), publickey-credentials-get=(self)"
Header always set Content-Security-Policy "base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests"
<Directory /var/www/html>
Options FollowSymLinks
AllowOverride All
+48 -6
View File
@@ -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 = '<span><strong>' + d.domain + '</strong><br><small class="text-muted">Approved: ' + d.confirmed_at + '</small></span>' +
'<button class="btn btn-sm btn-outline-danger" onclick="removeDomain(' + d.id + ')">Revoke</button>';
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 = '<div class="d-flex w-100 justify-content-between"><strong>' + label + '</strong><small class="text-muted">' + e.created_at + '</small></div>' +
'<small class="text-muted">' + (e.ip ? e.ip + ' &middot; ' : '') + (e.user_agent ? e.user_agent.substring(0, 60) + '...' : '') + '</small>' +
(e.details ? '<br><small>' + e.details + '</small>' : '');
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 = '<span><strong>' + (s.user_agent || 'Unknown device') + '</strong></span>';
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);
});
});
+7 -1
View File
@@ -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);
}
?>
?>
@@ -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]);
echo json_encode(['success' => true]);
+39 -15
View File
@@ -1,19 +1,25 @@
<?php
function secure_cookie_options(array $overrides = []): array
{
return array_merge([
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
], $overrides);
}
function secure_session_start(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
session_set_cookie_params([
session_set_cookie_params(secure_cookie_options([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
]));
session_start();
}
@@ -242,13 +248,9 @@ function clear_rate_limit(mysqli $conn, string $bucket, string $identifier = '')
function set_secure_cookie(string $name, string $value, int $expires): void
{
setcookie($name, $value, [
setcookie($name, $value, secure_cookie_options([
'expires' => $expires,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
]));
}
function delete_cookie(string $name): void
@@ -280,6 +282,10 @@ function normalize_redirect_target(?string $target): string
return '/account/';
}
if (normalize_redirect_host($parts['host']) === null) {
return '/account/';
}
return $target;
}
@@ -308,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;
}
@@ -321,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;
}
?>
+2 -2
View File
@@ -7,8 +7,8 @@ RUN apt-get update && \
pecl install redis && \
docker-php-ext-enable redis
# Enable SSL module for Apache
RUN a2enmod ssl
# Enable Apache modules
RUN a2enmod ssl headers
# Restart Apache to apply changes
RUN service apache2 restart