Skip to content

fix(config): close the last two redaction holes -- env() in headers, URL userinfo (#1207) - #306

Open
wshallwshall wants to merge 2 commits into
mainfrom
claude/fix-headers-envref-and-userinfo
Open

fix(config): close the last two redaction holes -- env() in headers, URL userinfo (#1207)#306
wshallwshall wants to merge 2 commits into
mainfrom
claude/fix-headers-envref-and-userinfo

Conversation

@wshallwshall

@wshallwshall wshallwshall commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Both sit inside surfaces the redactor already claimed to handle, which is why neither was visible from outside.

(b) env() ref in a headers table

The headers branch had no EnvRef arm, so an env() ref came back as the raw object with its default intact — not JSON-safe, and the default is a fallback secret by construction. The same env() on a top-level credential correctly emits {"env": key} with the default dropped.

The default is now dropped for every header, not only credential-shaped ones. The measured instance used X-Vendor-Thing, which matches no substring in the name rule — so gating the EnvRef arm on that rule would have left this exact case open. A header sourced from env() is a credential by intent; nobody env()-refs a Content-Type. The name heuristic is the wrong gate here, and it is precisely the gate that failed.

(c) Credential in URL userinfo

url="https://user:SECRET@host" was returned verbatim by both serializers while proxy_password on the same object masked — safe in the typed field, disclosed in the URL beside it.

Only the password half is replaced; the user, host and path survive. An operator diagnosing a connection needs to see which account and which host, and masking the whole URL would destroy the view rather than protect it. A control test asserts a URL without userinfo is left byte-identical — a masker that rewrote every URL would satisfy the leak assertions while silently mangling ordinary configuration.

Measured, both serializers

(b) BEFORE  -> EnvRef(key='acme_key', default='<secret>')   raw, default intact
    AFTER   -> {'env': 'acme_key'}                          default dropped
    control    Content-Type: application/json               untouched

(c) BEFORE  -> https://user:<secret>@host/y                 verbatim
               http://puser:<secret>@proxy:8080             verbatim
               proxy_password on the same object            '***'
    AFTER   -> https://user:***@host/y                      user/host/path preserved
    control    https://plain.invalid/path?q=1               untouched

Noticed in passing

proxy is another parameter-to-setting rename: the factory parameter is proxy, the emitted setting is proxy_url. Same boundary with_signing crosses (private_key to sign_private_key, #1106) — which is why the URL rule is a name set plus a suffix rule rather than a suffix rule alone.

95 tests green across the redaction and connection-API suites.

Provenance and what it unblocks

Both found by the parallel asvs-tracking-rework session's independent assessment of ASVS 15.3.1, alongside the odbc_params disclosure fixed as #1206.

With these closed, all three surfaces holding that cell at partial are addressed and it is due a re-read — by that session, not by me, since I authored all three fixes.

Process note against myself

The code comments here cited #1207 before the number was allocated. It happened to be next, so nothing collided — but "happened to be next" is exactly the reasoning alloc.ps1 exists to eliminate, and two sessions doing it simultaneously is the documented failure mode. Allocate, then write.

…URL userinfo (#1207)

Both sit INSIDE surfaces the redactor already claimed to handle, which is why
neither was visible from the outside.

(b) The headers branch had no EnvRef arm, so an env() ref in a headers table came
back as the RAW object with its `default` intact -- not JSON-safe, and the default
is a FALLBACK SECRET by construction. The same env() on a top-level credential
correctly emits {"env": key} with the default dropped. A hole inside the one
container the header rule already covered.

    THE DEFAULT IS NOW DROPPED FOR EVERY HEADER, not only credential-shaped ones.
    The measured instance used X-Vendor-Thing, which matches no substring in the
    name rule -- so gating the EnvRef arm on that rule would have left this exact
    case open. A header sourced from env() is a credential by intent; nobody
    env()-refs a Content-Type. The name heuristic is the wrong gate here and it is
    precisely the gate that failed.

(c) url="https://user:SECRET@host" was returned verbatim by both serializers while
proxy_password on the SAME object masked -- safe in the typed field, disclosed in
the URL beside it. Only the password half of the userinfo is replaced: the user,
host and path survive, because an operator diagnosing a connection needs to see
which account and which host, and masking the whole URL destroys the view rather
than protecting it. A control test asserts a URL WITHOUT userinfo is left
byte-identical, since a masker that rewrites every URL would satisfy the leak
assertions while mangling ordinary configuration.

`proxy` is another parameter-to-setting rename, noticed in passing: the parameter
is `proxy`, the emitted setting is `proxy_url`. Same boundary with_signing crosses
(#1106), which is why the URL rule is a NAME set plus a suffix rule rather than a
suffix rule alone.

95 tests green across the redaction and connection-API suites.

Both found by the asvs-tracking-rework session's assessment of ASVS 15.3.1,
alongside the odbc_params disclosure fixed as #1206. With these closed, all three
surfaces holding that cell at partial are addressed and it is due a re-read -- by
that session, not me, since I authored all three fixes.

PROCESS NOTE AGAINST MYSELF: the code comments here cited #1207 BEFORE the number
was allocated. It happened to be next, so nothing collided -- but "happened to be
next" is exactly the reasoning alloc.ps1 exists to eliminate, and two sessions
doing it at once is the documented failure. Allocate, then write.
proxy -> proxy_url makes THREE measured instances of one shape: a factory takes a
credential PARAMETER and emits it under a different SETTING name, while every
redaction control operates on the setting name. private_key ->
sign_private_key was #1106; proxy -> proxy_url is harmless only because the URL
rule happens to cover it, which is luck rather than design.

Neither existing guard covers the MAPPING. One reads parameter names, an
abstraction level from where redaction operates -- that is how #1106 walked
through it. The other reads emitted settings end to end and proves the OUTCOME
per factory, not that the parameter arrived anywhere at all.

Filed into this branch rather than its own, to avoid a fourth three-way conflict
on docs/BACKLOG.md while the engine queue is serialized; it is the route-onward
from the same finding as #1207.

Raised by the asvs-tracking-rework session, filed before it was forgotten.
@wshallwshall
wshallwshall enabled auto-merge (squash) August 9, 2026 15: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