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. + +