Skip to content

Scan failure messages are stored without credential redaction #43

Description

@BryanTegomoh

cliErrorMessage() at src/cli.ts:2898 strips API keys, bearer tokens and URL passwords out of every error the CLI prints. It is used at 22 call sites, all of them on the way to stderr.

The same string gets written to the scan database without it.

src/api.ts:733:

"fail-scan",
"--scan-id",
activeScan.id,
"--message",
(failure instanceof Error
  ? failure.message
  : String(failure)
).slice(0, 2400),

That value comes off the model event stream (api.ts:1191, api.ts:1196). It is stored in scans.failure_message (workbench_db.py:1777) and printed back out by scan-history show (scan-history-renderer.ts:213). Nothing on that path redacts it.

So a credential the CLI hides from the terminal still lands on disk.

What it looks like

I copied cliErrorMessage verbatim and ran four realistic upstream errors through it. First line is what you see, second is what gets stored.

stderr : error sending request for url (https://[redacted]@proxy.internal/v1/responses)
stored : error sending request for url (https://svc-account:s3cr3tP4ss@proxy.internal/v1/responses)

stderr : Incorrect API key provided: [redacted]
stored : Incorrect API key provided: sk-proj-ABCDEF1234567890

stderr : upstream rejected header Authorization: Bearer [redacted]
stored : upstream rejected header Authorization: Bearer eyJhbGciOiJIUzI1NiJ9padding

stderr : fetch to https://api.internal/v1/chat?api_key=[redacted] failed
stored : fetch to https://api.internal/v1/chat?api_key=SUPERSECRETVALUE123 failed

The first one is the easy path to hit. Point the tool at a proxied model provider whose base_url carries userinfo and let the provider be unreachable. classifyConnectionFailure (api.ts:1543) reads that as a network error, so you get the canned "The model service could not be reached" from cli.ts:2499 and never see the URL. The raw string with the password is already in workbench.sqlite3.

Why it leaves the machine

Dockerfile:52 sets CODEX_SECURITY_STATE_DIR=/output/.codex-security-state and compose.yaml:28 mounts /output to the host results directory. The database sits inside the bundle people archive and pass around.

The db file is 0600 (workbench_db.py:237), so I am not claiming local disclosure. The problem is that the value is stored unredacted, printed again by scan-history show, and carried along in the results directory.

Fix

Apply the redactor at api.ts:737. That means moving cliErrorMessage somewhere api.ts can import it.

Worth folding in while you are there. redactError at src/multiscan.ts:525 is a third copy, used for bulk-scan receipts, and it is missing the URL and query parameter rules. Same input, nothing removed:

in  : error sending request for url (https://svc-account:s3cr3tP4ss@proxy.internal/v1/responses)
out : error sending request for url (https://svc-account:s3cr3tP4ss@proxy.internal/v1/responses)

One shared helper for all three would close both gaps and stop them drifting apart again.

On the tests

tests-ts/cli-fixtures.ts:20 has a 50 entry list of secret shapes, including https://USER:PASSWORD@example.test/private. It is asserted in five test files, all on the stderr path. tests-ts/api.test.ts:1890 asserts the raw message reaching fail-scan, so the suite as it stands would not catch this.

Found on 0.1.1 by reading the source, not from a failed scan in the wild.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions