From 9f5142b0718a9159fbd48d1f5ff7297dbf758fa2 Mon Sep 17 00:00:00 2001 From: Marco0300 Date: Tue, 1 Sep 2026 18:58:18 +0200 Subject: [PATCH] fix: harden sessions and domain validation --- README.md | 2 +- app/Domain/Jobcard/TimeAggregator.php | 2 +- app/Domain/Jobcard/TimeCalculator.php | 2 +- app/Domain/SLA/SlaCalculator.php | 3 +++ config/bootstrap.php | 7 ++++++- public/index.php | 30 +++++++++++++++++---------- tests/JobcardCalculationsTest.php | 3 +++ tests/JobcardDomainTest.php | 2 +- tests/SlaDomainTest.php | 5 +++++ 9 files changed, 40 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9d1bfd8..5a79c64 100644 --- a/README.md +++ b/README.md @@ -33,4 +33,4 @@ find app config database public -type f -name '*.php' -print0 | xargs -0 -n1 php - Do not commit `.env` or production credentials. - Set `APP_KEY` to a long random value and keep it in a secrets manager in production. - Credential vault encryption and the remaining domain modules are scheduled in later phases. -- The initial schema is intentionally migration-ready but is delivered as an idempotent bootstrap SQL file for the first Docker increment. +- The initial schema is delivered as a Docker bootstrap SQL file. Apply it once to a new database; later releases should use versioned migrations. diff --git a/app/Domain/Jobcard/TimeAggregator.php b/app/Domain/Jobcard/TimeAggregator.php index 102dbf0..14d7aea 100644 --- a/app/Domain/Jobcard/TimeAggregator.php +++ b/app/Domain/Jobcard/TimeAggregator.php @@ -22,7 +22,7 @@ final class TimeAggregator { return $this->total(array_values(array_filter( $entries, - static fn ($entry): bool => is_array($entry) && (($entry['counts_toward_sla'] ?? true) === true) + static fn ($entry): bool => is_array($entry) && in_array($entry['counts_toward_sla'] ?? true, [true, 1, '1'], true) ))); } } diff --git a/app/Domain/Jobcard/TimeCalculator.php b/app/Domain/Jobcard/TimeCalculator.php index 1844f07..cefcf22 100644 --- a/app/Domain/Jobcard/TimeCalculator.php +++ b/app/Domain/Jobcard/TimeCalculator.php @@ -4,7 +4,7 @@ declare(strict_types=1); function calculate_duration_hours(?string $start, ?string $end, ?float $manualHours = null): ?float { if ($manualHours !== null) { - return $manualHours >= 0 ? round($manualHours, 2) : null; + return $manualHours > 0 ? round($manualHours, 2) : null; } if ($start === null || $end === null || !preg_match('/^\d{2}:\d{2}$/', $start) || !preg_match('/^\d{2}:\d{2}$/', $end)) { return null; diff --git a/app/Domain/SLA/SlaCalculator.php b/app/Domain/SLA/SlaCalculator.php index fd6f929..ef2ddff 100644 --- a/app/Domain/SLA/SlaCalculator.php +++ b/app/Domain/SLA/SlaCalculator.php @@ -3,6 +3,9 @@ declare(strict_types=1); function calculate_sla_usage(float $allocatedHours, array $hours): array { + if ($allocatedHours < 0) { + throw new InvalidArgumentException('Allocated hours must not be negative.'); + } $used = round(array_sum(array_map(static fn ($value): float => max(0.0, (float)$value), $hours)), 2); $remaining = round(max(0.0, $allocatedHours - $used), 2); $percentage = $allocatedHours > 0 ? round(($used / $allocatedHours) * 100, 2) : ($used > 0 ? 100.0 : 0.0); diff --git a/config/bootstrap.php b/config/bootstrap.php index 5d3182f..72e1dfc 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -35,9 +35,14 @@ function csrf_token(): string return $_SESSION['csrf']; } +function scalar_input(mixed $value, string $default = ''): string +{ + return is_scalar($value) ? (string)$value : $default; +} + function verify_csrf(): void { - $provided = (string)($_POST['_csrf'] ?? ''); + $provided = scalar_input($_POST['_csrf'] ?? null); if (!hash_equals((string)($_SESSION['csrf'] ?? ''), $provided)) { http_response_code(419); exit('Invalid CSRF token'); diff --git a/public/index.php b/public/index.php index a543e5d..30331b9 100644 --- a/public/index.php +++ b/public/index.php @@ -3,8 +3,11 @@ declare(strict_types=1); require_once __DIR__ . '/../config/bootstrap.php'; require_once __DIR__ . '/../app/Domain/Client/ClientValidator.php'; +require_once __DIR__ . '/../app/Domain/Jobcard/JobcardReference.php'; -session_set_cookie_params(['httponly' => true, 'secure' => !empty($_SERVER['HTTPS']), 'samesite' => 'Lax']); +ini_set('session.use_strict_mode', '1'); +$forwardedHttps = getenv('TRUST_PROXY') === '1' && scalar_input($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'; +session_set_cookie_params(['httponly' => true, 'secure' => !empty($_SERVER['HTTPS']) || $forwardedHttps, 'samesite' => 'Lax', 'path' => '/']); session_start(); function render_header(string $title): void @@ -12,7 +15,7 @@ function render_header(string $title): void $user = current_user(); echo '' . e($title) . ' · JOBcard'; if ($user) { - echo '