/** * Log scrubber (TASK-0134, ASVS L2 §14.4.10 / OWASP logging guidance). * * Redacts secret-bearing values from the strings that the API writes to * structured logs. The only request-coupled log paths in the API are the * global exception filter (`req.originalUrl` + exception message/stack) and * ad-hoc `logger.*` calls; anything that can carry a credential is * scrubbed at those boundaries: * * - `Authorization: Bearer ` header values * - JWT-shaped tokens (three base64url segments) * - factory + common third-party tokens (`fk_…`, `whsec_…`, `apk_…`, * `ghp_…`, `xox[baprs]-…`, `sk-…`, `AKIA…`) * - credential query parameters (`token=`, `refresh_token=`, `api_key=`, * `secret=`, `signature=`, `password=`, …) * - `Cookie:` / `Set-Cookie:` values * * The web/worker/realtime apps were audited for the same risk and have NO * request-coupled HTTP log paths that emit headers, cookies, or auth values * (the worker logs job/processor lifecycle; realtime logs channel lifecycle) * — so the scrubber is applied where the exposure exists (the API). See * docs/security/audit-report.md finding F-121. * * Deterministic + side-effect-free; safe to call on `undefined`-prone * values (non-strings pass through untouched). */ export const REDACTED = "[REDACTED]"; const BEARER = /\bBearer\s+[A-Za-z0-9\-._~+/]+={0,2}\b/gi; const JWT = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g; const TOKEN_PREFIX = /\b(fk|whsec|apk|ghp|gho|ghs|ghr|xox[baprs]|sk|AKIA)[A-Za-z0-9_-]{8,}\b/g; // Exact credential param names only (not substring matches, so an innocent // param can't be redacted): ?token=, ?refresh_token=, ?api_key=, … // Group 1 = the `?name=` prefix (preserved), group 2 = the value (redacted). const CRED_PARAM = /([?&](?:access_token|refresh_token|id_token|reset_token|token|api_key|apikey|secret|signature|password|session|session_id|state|code)=)([^&\s]*)/gi; const COOKIE = /\b(cookie|set-cookie):\s*[^\n,]+/gi; /** * Redact secret-looking values in a log string. Returns the input * unchanged (same type) when it is not a string. */ export function scrubSecrets(input: T): T { if (typeof input !== "string" || input.length === 0) return input; const s: string = input; let out = s; out = out.replace(BEARER, `Bearer ${REDACTED}`); out = out.replace(JWT, REDACTED); out = out.replace(TOKEN_PREFIX, REDACTED); out = out.replace(CRED_PARAM, `$1${REDACTED}`); out = out.replace(COOKIE, `$1: ${REDACTED}`); return out as T; }