diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 721fb8f6..73e08741 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -8449,6 +8449,24 @@ _FHIR_ID_RE.fullmatch("abc\n") -> False the fix **Source:** #1107's ASVS 1.2.2 dynamic-URL surface enumeration, 2026-08-13. Related: #1240 (the grammar gates on the message-derived path), #1239 (duplicated control-char predicate in the same file). +## 1243. close ASVS 1.2.2 on the merits: remove the verbatim flat FHIR query path and escape FHIR value separators + +> 🔢 **Filed 2026-08-13 -- OWNER-RULED, both limbs, and the two limbs are one item on purpose.** Value **7/10** -- Difficulty **5/10**. ASVS 1.2.2 asks that untrusted data be *"encoded according to its context"* when dynamically building URLs. `transports/fhir.py` gets the URL layer right and the **FHIR value layer** wrong, and separately ships an **unencoded path as the default**. Either one alone holds the cell at partial, so **closing one limb without the other does not close 1.2.2** -- that is why this is not two items. + +> **LIMB A -- REMOVE THE VERBATIM FLAT QUERY PATH.** `[egress].fhir_require_structured_params` ships **`False`** (`config/settings.py:2555`), so the default path is `transports/fhir.py:750`, `url = f"{url}?{search_part}"` -- the caller's search string appended **with no encoding at all**. The four screens above it at `:744-748` reject `#`, a second `?`, and a percent-decoded control character, and the comment at `:741` labels them *"Defense-in-depth (ASVS 1.2.2)"*. +> **Do not read that comment as a control. The screen is the gap.** It is a denylist of three shapes standing where contextual encoding belongs, and its own comment says why it stops there: it declines to break *"legitimate multi-param (&/=/|) searches"*. Anchoring `:744-748` as evidence for 1.2.2 would cite the statement of the shortfall as proof of the control. +> **The ruling is deletion, not a flipped default.** Setting `fhir_require_structured_params = True` would leave the unencoded sink reachable behind one config edit, which closes the cell on a default rather than on the absence of the sink -- a compensating control that a single setting removes. With **zero deployments** there is no migration cost to protect, so the simple correct end state wins outright: one code path, encoded by construction. + +> **LIMB B -- ESCAPE THE FHIR VALUE SEPARATORS `,` `|` `$` IN STRUCTURED VALUES.** `_encode_search_params` (`transports/fhir.py:671`) percent-encodes via `urlencode(..., quote_via=quote, safe="")`, which neutralises `&` and `=` at the URL layer. It does **not** neutralise `,`, `|` or `$`, which are **FHIR search-value separators**: they survive percent-decoding and arrive at the server with their separator meaning intact. Verified by execution -- `code=a,b` and `code=sys|val` reach the server as a two-value OR and a system-qualified token respectively, not as literals. A data value that happens to contain one therefore **changes the query's semantics**, which is parameter injection one layer above the URL. FHIR defines backslash escaping for exactly these characters; there is currently **no escaping of any kind** in the file. + +> **LIMB B DOES NOT TOUCH `conditional_query`, AND THAT IS THE WHOLE REASON IT IS SAFE.** The distinction is provenance, and it is easy to get backwards. A **structured value** is a single datum, so a separator inside it is always accidental and escaping it is always right. `conditional_query` is an **operator-authored whole query string** in which separators are the author's own syntax -- `CONNECTIONS.md:1479` documents `identifier=sys|val` as the intended shape and `tests/test_fhir_transport.py:210-221` pins it on both sinks (`If-None-Exist` at `:214`, the conditional-update URL at `:221`). **Escaping `conditional_query` would break a documented feature and fail a shipped test.** Its containment is a construction-time screen, which is **#1241**, already filed. Fixing this item must leave both assertions green. + +> **WHAT IS NOT IN SCOPE HERE, so nobody folds it in.** The `match`-versus-`fullmatch` grammar-gate defect and the missing READ-path control-char screen are **#1240**. The construction-time screen for operator config reaching URL and header sinks is **#1241**. The two identical control-char predicates are **#1239**. This item is the 1.2.2 encoding question only. + +> **RE-SCORE NOTE.** 1.2.2's residual of record currently reads, in full: *"fhir_require_structured_params ships False; on the default the flat search string is appended verbatim."* Limb A retires that sentence. The cell cannot reach an honest pass on Limb A alone, because the structured path it would then mandate still leaks the value-layer separators -- which is the finding that produced Limb B, and which had been filed as safe by everyone who looked at it, including the dispatcher who raised it. + +**Cluster:** Transports / FHIR egress. **Priority:** P2. **Verdict:** build. **Severity:** no deployment axis -- zero instances; on first deployment a Handler passing an unsanitised value into a FHIR search **would** be able to alter the query's meaning. + ## 1242. `asvs-apply-cells.py` is a lossy writer in four distinct ways, and its own preservation guard is blind to three of them > 🔢 **Filed 2026-08-13 -- PENDING ACTIVE DATA LOSS, not a latent hazard. The next routine `--apply` destroys a whole commit's worth of structural evidence and prints a clean bill of health.** Value **8/10** -- Difficulty **4/10**. Vault-only security tooling, so there is no deployment axis; what it destroys is the **evidence record** the ASVS cells rest on. Findings below were adversarially verified (4 of 4 lenses), which corrected two errors in the original framing -- see the amendment note.