fix(secrets): credentials reaching surfaces they must never reach - #34
Merged
Conversation
Shared shape: redaction existed and was applied to the OBVIOUS carriers, then skipped one that is just as credential-bearing in practice. 1. Recorded traffic never redacted the QUERY STRING. Headers and bodies were cleaned. `?api_key=sk-live-…` and `?access_token=…` shipped VERBATIM into simulation.json, the console API, the MCP tool output and the signed evidence bundle — the artifact whose whole purpose is to be handed to someone else. REDACT_BODY_KEYS already named `api_key` and `access_token`, so the module knew they were secrets and never applied the rule to the query. Compounding it, the redaction counter never saw them, so a sample whose only secrets were in query strings reported ZERO redactions — affirmatively describing itself as clean. Fixed at `_record`, the single point all five sources (HAR, XC tenant logs, JSONL, probes) build through, and pinned so a sixth source that builds a RequestRecord directly cannot silently skip it. Values are replaced, not dropped: a policy matcher is judged against the shape of the request. 2. GET /api/config returned the audit-sink URL in full. A sink URL routinely carries credentials in the URL itself — basic auth, and the Splunk-HEC shape `…/services/collector/<token>`. Both were returned verbatim by the console API. `audit_sink.redact` exists for exactly this and every other surface already used it; the code comment asserting "the URL is NOT secret" was simply wrong for those shapes. AND the fix nearly introduced a worse bug: the settings form posts every non-empty field, so showing the ellipsis form in an editable input meant one Save wrote `https://…@host/…` into .env — silently destroying the sink, the one component whose failure mode is "no record of anything". The field now shows the redacted value as a PLACEHOLDER and stays empty, and the server independently refuses to persist an echoed redaction, because a guard that lives only in the page is not a guard. 3. The Setup page reported a live credential as unset. (Found while walking the GUI, not in the review.) /api/config read .env only, so a value from the process environment — how the documented BIG-IP setup works, and how CI and any container run — rendered as "(unset)". The page said `BIGIP_URL (unset)` directly above a panel talking to the appliance over that URL. Not cosmetic: an operator who believes it is unset sets it, the page writes .env, and the environment value still wins, so the change reads as applied and is not. Now three states — env-file / environment / unset — with the page warning that .env will not take effect while the variable is set. 4. test_the_client_never_lets_the_password_reach_an_error_string was VACUOUS. It asserted the password was absent from a mocked body of "boom" — a string that never contained it — so `BigIP._redact` could be deleted outright and it still passed, in the one test named for a credential leak. It now mocks the realistic shape (AS3 rejecting a declaration and echoing it back, which legitimately carries credentials), asserts the redaction is VISIBLE rather than a silent truncation, and asserts the diagnostic survives. A second test covers the transport-error branch. 11 new tests; suite 1054 -> 1065. Mutation-verified: reintroducing the leaks fails 6 of them, and deleting BigIP._redact's body fails both password tests (it failed neither before). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fifth PR from the deep-dive review. Shared shape: redaction existed, was applied to the obvious carriers, and skipped one that is just as credential-bearing in practice.
1. Recorded traffic never redacted the query string
Headers and bodies were cleaned. The query was not:
That ships verbatim into
simulation.json, the console API, the MCP tool output and the signed evidence bundle — the artifact whose entire purpose is to be handed to someone else.REDACT_BODY_KEYSalready namedapi_keyandaccess_token: the module knew these were secrets and simply never applied the rule to the query.Compounding it, the redaction counter never saw them, so a sample whose only secrets were in query strings reported zero redactions — affirmatively describing itself as clean. "We did not check this" rendering as "this is clean", on the evidence artifact.
Fixed at
_record, the single point all five sources (HAR, XC tenant logs, JSONL, probes) build through — and pinned, so a sixth source that constructs aRequestRecorddirectly cannot silently skip it. Values are replaced, not dropped: a policy matcher is judged against the shape of the request, so a parameter that vanishes changes what is being tested.2.
GET /api/configreturned the audit-sink URL in fullBoth credential shapes, in full, from the console API.
audit_sink.redactexists for exactly this and every other surface already used it — the code comment asserting "The URL is NOT secret" was simply wrong for basic-auth and HEC-token-in-path forms.The fix nearly introduced something worse
The settings form posts every non-empty field. Showing
https://…@host/…in an editable input meant one Save would write the ellipsis into.env— silently destroying the sink, which is the one component whose failure mode is "no record of anything".The field now shows the redacted value as a placeholder and stays empty, so a save cannot echo it. And the server independently refuses to persist an echoed redaction, because the endpoint is reachable directly and a guard that lives only in the page is not a guard. A test proves a real change still saves — the field is not read-only.
3. The Setup page reported a live credential as unset
Found while walking the GUI, not in the review.
/api/configread.envonly, so a value from the process environment — which is how the documented BIG-IP setup works, and how CI and any container run — rendered as(unset):Two panels on one screen contradicting each other about whether a fact was established. Not cosmetic: an operator who believes a credential is unset sets it, this page writes
.env, and the process keeps using the environment value that still wins — so the change reads as applied and silently is not, on a security-relevant credential.Now three states —
env-file/environment/ unset — with the page warning that.envwill not take effect while the variable is set.4. The password-leak test was vacuous
test_the_client_never_lets_the_password_reach_an_error_stringasserted the password was absent from a mocked body of"boom"— a string that never contained it.BigIP._redactcould be deleted outright and it still passed. In the one test named for a credential leak.It now mocks the realistic shape: AS3 rejecting a declaration and echoing the submitted document back, which legitimately carries credentials (remote logging targets, pool member auth). It asserts the redaction is visible rather than a silent truncation, and that the diagnostic survives — an unreadable error is its own failure. A second test covers the transport-error branch, where httpx can put a credential-bearing URL in the message.
Verification
BigIP._redact's body fails both password tests — it failed neither before.Progress
20 of 27 closed; 7 open, tracked in
BACKLOG.md. The rest are being verified and planned in parallel — mostly reporting-honesty and surface-parity items, plus one vacuous test guarding the four-place agent registration.🤖 Generated with Claude Code