From f9e8638b2443ebf2da2e11eb12d1a8d60bf7c9d6 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 7 Aug 2026 22:59:34 -0700 Subject: logging: redact auth headers; set cookie flags on session token (S5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The request logger persisted full req.headers to the "req" log, including the Authorization bearer token and the token cookie — both non-expiring JWTs, so log-read access became account takeover. Redact the authorization and cookie headers. Also set httpOnly/secure/sameSite on the token cookie so it isn't readable by injected JS or sent over plaintext HTTP. (Revocation is already handled by the evicted set.) Co-Authored-By: Claude Opus 4.8 --- lib/app.ts | 13 ++++++++++++- routes/users.ts | 16 ++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/app.ts b/lib/app.ts index 4cb4ef46..1a2102bd 100644 --- a/lib/app.ts +++ b/lib/app.ts @@ -33,6 +33,17 @@ const redactBody = (body: any) => { return copy; }; +// Never persist session credentials to the request log. The Authorization +// bearer token and the token cookie are full account credentials (JWTs that +// don't expire) — logging them turns log-read access into account takeover. +const REDACT_HEADERS = ["authorization", "cookie"]; +const redactHeaders = (headers: any) => { + if (!headers || typeof headers !== "object") return headers; + const copy: any = { ...headers }; + for (const h of REDACT_HEADERS) if (h in copy) copy[h] = "[redacted]"; + return copy; +}; + // app.addHook("onRequest", async (req) => { // reqLogger.info({ url: req.raw.url, id: req.id }); // }); @@ -64,7 +75,7 @@ app.addHook("preHandler", async (req) => { method: req.method, url, ip, - headers: req.headers, + headers: redactHeaders(req.headers), query: req.query, body: redactBody(req.body), user: (req.user as any)?.username, diff --git a/routes/users.ts b/routes/users.ts index eafcd74a..ed2d3861 100644 --- a/routes/users.ts +++ b/routes/users.ts @@ -454,7 +454,13 @@ export default { const payload = { id: user.id }; const token = jwt.sign(payload, config.jwt); - res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); + res.cookie("token", token, { + expires: new Date(Date.now() + 432000000), + path: "/", + httpOnly: true, + secure: true, + sameSite: "lax", + }); user = pick(user, whitelist); res.send({ user, token }); } catch (e) { @@ -514,7 +520,13 @@ export default { const payload = { id: user.id }; const token = jwt.sign(payload, config.jwt); - res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); + res.cookie("token", token, { + expires: new Date(Date.now() + 432000000), + path: "/", + httpOnly: true, + secure: true, + sameSite: "lax", + }); user = pick(user, whitelist); res.send({ user, token }); } catch (e) { -- cgit v1.2.3