Files
jakach-login/app-code/api/utils/security.php
T
janis d82a08f77b
Deploy / deploy (push) Successful in 33s
adding enhanced csrf protection
2026-05-06 09:07:48 +02:00

155 lines
4.0 KiB
PHP

<?php
function secure_session_start(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
$is_https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443);
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => $is_https,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
function json_response(array $data, int $status_code = 200): void
{
http_response_code($status_code);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
function require_same_origin_request(): void
{
if (!in_array($_SERVER['REQUEST_METHOD'] ?? 'GET', ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
return;
}
$host = $_SERVER['HTTP_HOST'] ?? '';
$source = $_SERVER['HTTP_ORIGIN'] ?? $_SERVER['HTTP_REFERER'] ?? '';
if ($source === '') {
return;
}
$source_host = parse_url($source, PHP_URL_HOST);
$source_port = parse_url($source, PHP_URL_PORT);
if ($source_host && $source_port) {
$source_host .= ':' . $source_port;
}
if (!$source_host || !hash_equals(strtolower($host), strtolower($source_host))) {
json_response(['success' => false, 'message' => 'Invalid request origin.'], 403);
}
}
function csrf_token(): string
{
if (empty($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function print_csrf_script(): void
{
echo '<script>window.csrfToken = ' . json_encode(csrf_token()) . ';</script>';
}
function require_csrf_token(): void
{
if (!in_array($_SERVER['REQUEST_METHOD'] ?? 'GET', ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
return;
}
$token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? $_POST['csrf_token'] ?? '';
if (empty($_SESSION['csrf_token']) || !is_string($token) || !hash_equals($_SESSION['csrf_token'], $token)) {
json_response(['success' => false, 'message' => 'Invalid CSRF token.'], 403);
}
}
function require_logged_in(): void
{
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true || empty($_SESSION['id'])) {
json_response(['success' => false, 'message' => 'Not logged in'], 401);
}
}
function is_admin_session(): bool
{
return !empty($_SESSION['permissions'])
&& is_string($_SESSION['permissions'])
&& isset($_SESSION['permissions'][0])
&& $_SESSION['permissions'][0] === '1';
}
function remember_token_hash(string $token): string
{
return hash('sha256', $token);
}
function set_secure_cookie(string $name, string $value, int $expires): void
{
$is_https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443);
setcookie($name, $value, [
'expires' => $expires,
'path' => '/',
'secure' => $is_https,
'httponly' => true,
'samesite' => 'Lax',
]);
}
function delete_cookie(string $name): void
{
set_secure_cookie($name, '', time() - 3600);
}
function normalize_redirect_target(?string $target): string
{
$target = trim((string) $target);
if ($target === '') {
return '/account/';
}
if (preg_match('/[\r\n]/', $target)) {
return '/account/';
}
if (str_starts_with($target, '/') && !str_starts_with($target, '//')) {
return $target;
}
$parts = parse_url($target);
if (!$parts || empty($parts['scheme']) || empty($parts['host'])) {
return '/account/';
}
if (!in_array(strtolower($parts['scheme']), ['http', 'https'], true)) {
return '/account/';
}
return $target;
}
function append_auth_token_to_redirect(string $redirect, string $auth_token): string
{
$separator = str_contains($redirect, '?') ? '&' : '?';
return $redirect . $separator . 'auth=' . rawurlencode($auth_token);
}
?>