Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.


Loading