Skip to content

backlog: close #1106 (already shipped) and file #1223 -- http_auth_user leaks verbatim - #332

Merged
wshallwshall merged 1 commit into
mainfrom
w3-1106-and-authuser
Aug 11, 2026
Merged

backlog: close #1106 (already shipped) and file #1223 -- http_auth_user leaks verbatim#332
wshallwshall merged 1 commit into
mainfrom
w3-1106-and-authuser

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Closes BACKLOG #1106 (already shipped -- see below) and files BACKLOG #1223.

#1223 -- http_auth_user leaks verbatim, and the guard's vocabulary cannot see it

Confirmed by running the shipped redactor on main, not by reading it:

basic_user           redacted=True      http_auth_user       redacted=False   <-- leaks
proxy_user           redacted=True      http_auth_password   redacted=True
username             redacted=True      sign_private_key     redacted=True
ws_username          redacted=True
credential_username  redacted=True

config/wiring.py:847 carries {username, basic_user, proxy_user, ws_username, credential_username} --
masked defence-in-depth, on the stated ground that a username names a principal and can leak
directory structure. http_auth_user is written at transports/http_auth.py:477, across the same
parameter-to-setting boundary #1106 was entirely about
: with_signing renames private_key to
sign_private_key; with_http_digest renames user to http_auth_user. Same seam, same omission,
different key.
It is the sixth member of a five-member class.

Stated in the conditional (this is a beta with zero deployments): on a first deployment this would
emit an HTTP auth username verbatim from both serializers. It is a defence-in-depth leak of a principal
name, not of a password -- http_auth_password is correctly masked.

The structural half: why two independent-looking guards were both green

The tuple deciding where a sentinel gets injected was:

CREDENTIAL_ISH = ("password", "secret", "token", "key", "credential", "passphrase")

So _is_credential_param("user") is False, _is_credential_param("username") is False, and no guard
had ever injected into a username parameter at all.
credential_username matched only by accident,
through credential.

And test_credential_parameter_mapping.py inherits the same blindness by importing the same predicate
-- two independent-looking checks that are blind in exactly the same place by construction, because
they share the real predicate rather than a copy.

Proved by mutation, not by reading: widening the tuple by the single word user reds exactly
with_http_digest -> http_auth_user and nothing else.

The inversion was examined and rejected on evidence

The lane was dispatched believing deny-by-default was probably the structural answer. Measured over the
real 189-key set it is worse in both directions: it over-redacts 14 (four file paths, two URLs, a DB
column name, the odbc_*_key keyword indirections) and under-redacts 4 that are currently masked. And
decisively -- it would not have caught this defect: http_auth_user contains none of its six
substrings.

So the allowlist and the shape rule fail on the same key. The defect was never "the list is a list";
it is that nobody derived the domain. The fix that actually stops the next omission is a test deriving
the injection vocabulary from _SECRET_SETTING_KEYS itself -- red today without the widening, naming
all six usernames, and unable to narrow silently the way a hand-maintained tuple can.

#1106 was already fixed, and the banner says so in those words

e7566082 (PR #300) landed 2026-08-09; _is_secret_setting names sign_private_key and
sign_private_key_password explicitly at config/wiring.py:830. Verified on main: both redact from
both serializers, and it is pinned rather than incidentally green -- reverting that arm turns four
tests red.

The banner read "not started" and a lane was dispatched to build it. A machine-written status field
tracks status, not whether the work exists. The corrected banner records that plainly, and records that
the lane recovering the cost by finding #1223 was luck, not design.

Ledger-only. Verified: #1223 free in the live ledger and the archive; #1106 flips to a single closed
banner; parse_items reads 252 items, 184 open; merges clean onto current main.

…oving it

TWO CORRECTIONS, AND THE FIRST IS MINE.

#1106's banner read 'not started'. It shipped 2026-08-09 as e756608
(PR #300): _is_secret_setting names sign_private_key and
sign_private_key_password explicitly at config/wiring.py:830, and I
confirmed both return *** from both serializers on main. The lane also
verified it is PINNED rather than incidentally green -- reverting that arm
turns four tests red.

I READ 'not started' AND BELIEVED IT, then dispatched a lane to build
something already on main for two days. That is the same error class as
the #1219 duplicate, one level up, and I committed it AFTER writing the
memory entry about exactly this -- a status field believed instead of
measured. The banner is a machine-written field; whether the work exists
is not something it tracks.

The lane recovered the cost by finding a real defect while proving the
item was already done, but that was luck rather than design, and the
banner now says so.

#1223 -- http_auth_user LEAKS VERBATIM. Measured by running the shipped
redactor on main, not inferred:

  basic_user -> ***   proxy_user -> ***   username -> ***
  http_auth_password -> ***
  http_auth_user -> SYNTHETIC-ACCOUNT      <- verbatim, both serializers

It is the SIXTH member of a FIVE-member class: wiring.py:847 carries
{username, basic_user, proxy_user, ws_username, credential_username},
masked defence-in-depth because a username names a principal. http_auth_
user is written at transports/http_auth.py:477 across the SAME
parameter-to-setting boundary #1106 was entirely about.

THE STRUCTURAL HALF IS WHY BOTH GUARDS WERE GREEN: CREDENTIAL_ISH, the
tuple deciding where a sentinel is INJECTED, lacked 'user', so
_is_credential_param('user') is False and no guard had ever injected into
a username parameter at all -- credential_username matched only by
accident via 'credential'. The mapping test inherits the same blindness
by importing the same predicate, so two independent-looking checks share
one blind spot. Proved by mutation: widening the tuple by one word reds
exactly with_http_digest -> http_auth_user and nothing else.

AND THE INVERSION WAS EXAMINED AND REJECTED ON EVIDENCE rather than
adopted because it sounded structural. Over the real 189-key set it
over-redacts 14 (paths, URLs, a DB column, keyword-name indirections) and
under-redacts 4 it currently masks -- and decisively, IT WOULD NOT HAVE
CAUGHT THIS DEFECT, since http_auth_user contains none of its six
substrings. Allowlist and shape rule fail on the same key, because the
defect was never that the list is a list; it was that nobody derived the
domain.

Gates: 488 items each declaring exactly one status; ledger_check clean;
doc guards 48 passed. Live 252, open 184.
@wshallwshall
wshallwshall merged commit 2e17b6b into main Aug 11, 2026
33 checks passed
@wshallwshall
wshallwshall deleted the w3-1106-and-authuser branch August 11, 2026 18:58
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