From 402ac28ac33f1e7868d2be9781535d25176a66c8 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Tue, 11 Aug 2026 13:41:34 -0500 Subject: [PATCH] ledger: #1106 was SHIPPED two days ago, and file #1223 found while proving it TWO CORRECTIONS, AND THE FIRST IS MINE. #1106's banner read 'not started'. It shipped 2026-08-09 as e7566082 (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. --- docs/BACKLOG.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 2d625e32..1cadd8ac 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -5798,7 +5798,7 @@ than closed. ## 1106. `redacted_settings` does not redact the JWS signing key or its passphrase -> ๐Ÿ”ข **Filed 2026-08-08 - not started. EXECUTED against `main` 166634c9, not inferred.** Value **8/10** ยท Difficulty **2/10**. `_is_secret_setting` (`messagefoundry/config/wiring.py:686`) matches `_SECRET_SETTING_KEYS` plus the `body_secret_value_` prefix. It does **not** match `sign_private_key` or `sign_private_key_password` -- the exact names `with_signing` writes into `spec.settings` at `messagefoundry/transports/signing.py:505-506`. Both are therefore returned **verbatim** by `redacted_settings`, which is the scrubber for `GET /connections/{name}/metadata` and for `graph --json`. The `private_key` that IS in the registry (`wiring.py:623`) is the **SFTP** key -- a different setting. +> โœ… **SHIPPED 2026-08-09 (`e7566082`, PR #300) -- this banner read "not started" until 2026-08-11 and was WRONG for two days.** Value **8/10** -- Difficulty **2/10**. `_is_secret_setting` now names `sign_private_key` and `sign_private_key_password` explicitly (`config/wiring.py:830`), so both return `***` from **both** serializers while `sign_key_id` and `sign_algorithm` correctly stay readable. **Verified pinned rather than incidentally green:** reverting that arm turns **four** tests red, including the redaction-domain case and two credential-parameter-mapping cases. **The stale banner cost a dispatched lane.** A session read *"not started"*, believed it, and sent a worker to build something already on `main` for two days -- the same error class as filing a duplicate, one level up: **a status field believed instead of measured.** The lane recovered the cost by finding **#1223** while proving this item was already done, but that was luck, not design. **Cluster:** Security / secret disclosure. **Priority:** P1. **Verdict:** build. **Severity:** on first deployment this would return a **private signing key and its passphrase** to any caller holding @@ -7724,3 +7724,24 @@ gate is the wrong shape, validation of the walk is the right one. **Cluster:** Testing / instruments. **Priority:** P2. **Verdict:** build. **Severity:** conditional -- no product effect; the shipped `decode_frame` behaviour is correct. It is a red that blocks unrelated PRs and cannot be attributed to any of them. +## 1223. `http_auth_user` leaks verbatim: the sixth member of a five-member class, and the injector's vocabulary cannot see it + +> ๐Ÿ”ข **Filed 2026-08-11 -- MEASURED by executing the shipped redactor on `main`, not inferred.** Value **7/10** -- Difficulty **2/10** -- _quick win_. `with_http_digest` writes `http_auth_user` into `spec.settings` (`transports/http_auth.py:477`), and `_is_secret_setting` does not name it. Run against `origin/main`: + +> ``` +> 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.** `config/wiring.py:847` carries the engine's own username set -- `{"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 the same kind of thing, written across the **same parameter-to-setting boundary** that `#1106` was entirely about (`with_signing` renaming `private_key` to `sign_private_key`; `with_http_digest` renaming `user` to `http_auth_user`). On a first deployment it would return a **Digest auth username** to any caller holding `Permission.MONITORING_READ`, and print it to stdout, a CI log and the IDE graph view. + +> **WHY BOTH GUARDS WERE GREEN OVER IT, AND THIS IS THE STRUCTURAL HALF.** `CREDENTIAL_ISH` -- the tuple deciding **where a sentinel is injected** -- was `("password","secret","token","key","credential","passphrase")`. So `_is_credential_param("user")`, `("username")` and `("proxy_user")` all return **False**, and **no guard had ever injected into a username parameter at all**; `credential_username` matched only by accident, via `"credential"`. `test_credential_parameter_mapping.py` **inherits the same blindness by importing the same predicate**, so two independent-looking checks share one blind spot. Proved by mutation rather than by reading: widening the tuple by the single word `user`, changing nothing else, turns the guard red on **exactly `with_http_digest -> http_auth_user` and nothing else**. + +> **THE ALLOWLIST WAS EXAMINED AND KEPT, ON EVIDENCE.** Running the obvious inversion (`key|secret|password|passphrase|token|credential`) over the real 189-key set is **worse in both directions at once**: it **over-redacts 14** -- four file paths, two URLs, a DB column name, and the keyword-name indirections `odbc_password_key` / `odbc_user_key` -- which is how an operator's diagnostic view dies and the control gets switched off; and it **under-redacts 4** it currently masks, reopening `username`, `basic_user`, `proxy_user`, `ws_username`. **Decisively: the inversion would NOT have caught this defect** -- `http_auth_user` contains none of those six substrings. Allowlist and shape rule fail on the *same* key, because the defect was never *"the list is a list"*; it was that **nobody derived the domain**. + +> **Scope:** classify `http_auth_user`, and widen `CREDENTIAL_ISH` so the injector can see username-shaped parameters. **What stops the next omission** is the guard already written for this: a test deriving the injection vocabulary **from `_SECRET_SETTING_KEYS` itself**, failing if the engine ever classifies a key whose shape the injector cannot reach. It is red today without the widening and names all six usernames. **How to prove a fix:** it must red when `http_auth_user`'s classification is reverted, AND the vocabulary test must red when a classified key's shape is removed from the tuple -- two directions, because this defect lived in the gap between them. + +**Cluster:** Security / secret disclosure. **Priority:** P2. **Verdict:** build. **Severity:** conditional -- a credential-adjacent disclosure in a shipped default path; no PHI, and zero deployments, so nothing is exposed today. + +