From c2cc9c72e6f0199326712e54b1d0fdf295aa2f51 Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:25:37 +0100 Subject: [PATCH] fix(session): write expires_at in UTC, not local time (audit 6c-2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CreateSession wrote FormatDateTime(IncHour(Now,24)) = local, but RequireCSRF and CleanupExpired compare against SQLite datetime('now') = UTC. On negative UTC offsets the CSRF window fell short of the 24h auth window: between the two, GETs authed fine but every write got 403 "Invalid CSRF token". Now IncHour(NowUTC,24) on write + LExpires < NowUTC in Authenticate, so all three readers are UTC-consistent. Last FormatDateTime(...,Now) in the backend — closes §2.2 for real. NowUTC + DateUtils already in uses. Co-Authored-By: Claude Opus 4.8 --- CODE_AUDIT.md | 7 +++++++ delphi-backend/Source/PM.Session.pas | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CODE_AUDIT.md b/CODE_AUDIT.md index edf2c0d..67c2826 100644 --- a/CODE_AUDIT.md +++ b/CODE_AUDIT.md @@ -615,6 +615,13 @@ sous newKey*. C'est le filet qui aurait attrapé ça. ### 6c-2. 🟠 Expiry de session écrite en local, lue en UTC (corrige §2.2) +> ✅ **RÉSOLU 2026-07-16.** `CreateSession` écrit `IncHour(NowUTC, 24)` et +> `Authenticate` compare `LExpires < NowUTC` ([PM.Session.pas](delphi-backend/Source/PM.Session.pas)). +> Les 3 lecteurs (Authenticate, RequireCSRF, CleanupExpired) sont désormais tous +> sur UTC. `NowUTC` + `System.DateUtils` déjà dans les uses → aucun uses touché. +> §2.2 est maintenant réellement clos (c'était le dernier `FormatDateTime(..., +> Now)` du backend). Sessions existantes : expirent sous 24 h, purgées au boot. + [`CreateSession`](delphi-backend/Source/PM.Session.pas:155) écrit `FormatDateTime('yyyy-mm-dd hh:nn:ss', IncHour(Now, 24))` — heure **locale**. C'est le dernier `FormatDateTime(..., Now)` du backend et il contredit la règle diff --git a/delphi-backend/Source/PM.Session.pas b/delphi-backend/Source/PM.Session.pas index 8ea2f7c..57549ad 100644 --- a/delphi-backend/Source/PM.Session.pas +++ b/delphi-backend/Source/PM.Session.pas @@ -86,7 +86,8 @@ begin LQ.Free; end; - if (LExpires <> 0) and (LExpires < Now) then + // expires_at is now UTC (see CreateSession) — compare against UTC too. + if (LExpires <> 0) and (LExpires < NowUTC) then begin DeleteSessionByTokenHash(LTokenHash); TJSONHelper.SendError(AResponse, 401, 'Session expired'); @@ -152,7 +153,11 @@ begin ACSRFToken := RandomHex(32); LTokenHash := SHA256Hex(AToken); // YYYY-MM-DD HH:NN:SS, +24h, server local time (api.php uses date() = local) - LExpires := FormatDateTime('yyyy-mm-dd hh:nn:ss', IncHour(Now, 24)); + // UTC like everywhere else — RequireCSRF + CleanupExpired compare against + // SQLite datetime('now') which IS UTC. Writing local here made the CSRF + // window fall short on negative-offset zones (§6c-2): "Invalid CSRF token" + // on writes while GETs still authed. + LExpires := FormatDateTime('yyyy-mm-dd hh:nn:ss', IncHour(NowUTC, 24)); DB.Lock; try