Skip to content

fix(secrets): credentials reaching surfaces they must never reach - #34

Merged
henleda merged 1 commit into
mainfrom
fix-secret-redaction
Aug 5, 2026
Merged

fix(secrets): credentials reaching surfaces they must never reach#34
henleda merged 1 commit into
mainfrom
fix-secret-redaction

Conversation

@henleda

@henleda henleda commented Aug 5, 2026

Copy link
Copy Markdown
Owner

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:

QUERY: {'api_key': ['sk-live-SECRET123'], 'access_token': ['tok-ABC'], 'id': ['7']}
hdrs : {}                                  <- redacted
body : {'password': '[redacted]', ...}     <- redacted

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_KEYS already named api_key and access_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 a RequestRecord directly 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/config returned the audit-sink URL in full

value: 'https://hec:hunter2SECRET@splunk.example.com:8088/services/collector/TOK-ABC'
LEAKS the basic-auth password: True
LEAKS the HEC token: True

Both credential shapes, in full, from 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 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/config read .env only, 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):

/api/config     BIGIP_URL {'set': False, 'value': ''}
/api/bigip-lab  {"configured": true, "reachable": true, "as3": "3.56.0"}

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 .env will 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_string asserted the password was absent from a mocked body of "boom" — a string that never contained it. 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 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

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

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>
@henleda
henleda merged commit cb81374 into main Aug 5, 2026
4 checks passed
@henleda
henleda deleted the fix-secret-redaction branch August 5, 2026 02:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant