fix(security): sanitize the API-key prefix logged on authentication failure - #2569
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Review (agent half of the gate; Codex credits exhausted, SC-9). One fresh-context reviewer (read-only, Opus 5) at exact head 9e63535: verdict SHIP, no CRITICAL/HIGH. Confirmed: the prefix is sliced to eight characters before it is sanitized, so stripping can only shorten it and no token material past index 7 can reach the log on any branch (short token, empty-after-strip, split surrogate pair); no other log statement in Findings and dispositions:
The fix commit adds one test and rewords two comments and one doc sentence; no production logic changed, so no further review pass is owed. Merge gate: hosted ci-required green at 2410962 plus the aging floor. |
Summary
The MCP key-not-found branch in
ApiKeyMiddlewarelogged a raw 8-character slice of the presentedbearer token. That token is unauthenticated caller input, so the slice could carry CR, LF, U+2028 and
C1 controls straight into a plain-text log sink and forge an extra log line (CWE-117).
backend/src/Taskdeck.Api/Middleware/ApiKeyMiddleware.csnow slices the 8-character prefix firstand sanitizes it afterwards, through the existing
LogSanitizer.StripControlChars. Taking theslice first means stripping can only shorten the prefix, never pull further token material into the
log line, so at most 8 characters of the presented token are ever logged whatever the input. A
prefix that sanitizes to nothing is reported as
shortrather than as an empty value, so anoperator never sees a blank field; that branch is defensive only, since the format gate above
requires the token to start with the printable
tdsk_literal.LogControlCharacterSanitizer.Strip(behind bothLogSanitizerandLogValueSanitizer) now alsodrops Unicode format characters, general category Cf. That covers the zero-width and bidirectional
set called out on the issue: U+200B..U+200F, U+202A..U+202E, U+2060..U+2064, U+FEFF, plus the soft
hyphen. They render as nothing while hiding or reordering the text around them. Surrogates are
category Cs, not Cf, so valid surrogate pairs are untouched and unpaired halves stay with the
existing
IsUnpairedSurrogaterule. U+2028/U+2029 were already handled and are unchanged.docs/security/SECURITY_LOGGING_REDACTION.mdgains one bullet recording what that sanitizerstrips, plus the prefix-before-sanitize ordering.
Exception messages passed as the exception argument to
LogErrorare deliberately left alone. Thatis standard structured logging: the exception is a first-class argument and the sink owns its
rendering. It is recorded here as declined rather than fixed.
Root cause
ApiKeyMiddleware.InvokeAsyncbuilt the log argument astoken.Length >= 8 ? token[..8] : "short"with no sanitizer in the path, unlike every other request-derived log argument in the Api layer.
tokenisAuthorizationheader content afterBeareris stripped and the value is trimmed, sointerior control characters survive to the log call. The trailing gap was the sanitizer itself: it
stripped C0, DEL, C1 and the line/paragraph separators, but not the invisible Cf format characters.
Verification
All commands run from the worktree
C:/Users/jekyt/source/Taskdeck-Beta/.worktrees/codex-2519-apikey-log-prefix.Red first, against the unfixed middleware:
Failing assertions:
Green after the fix:
The
BatchExecuteProposalsApiTestscommand-shape flake from #2399 did not appear in either full run.Not verified
ci-requiredwill repeat the solution test.ILoggerformatter produces, not on rendered console or file output, so per-sink escapingbehaviour is unproven either way.
made of every existing caller of
LogValueSanitizerto confirm none of them logs a value where aformat character is load-bearing. Nothing in the suite depends on one surviving.
Risk notes
API-key prefix. The effect is subtractive only: characters that render as nothing are dropped.
Length-sensitive callers are unaffected in kind, since stripping already could shorten a value.
characters into it. That is the intended trade: the value is a triage aid, not an identifier, and
the persisted
ApiKey.KeyPrefix_column is unrelated and unchanged.CharUnicodeInfo.GetUnicodeCategoryis called per character on the sanitizing path. It is a tablelookup, and
Stripis only on error and diagnostic paths.shape are untouched, and the existing
ApiKeyMiddlewaretests that pin them still pass.Closes #2519