v0.208.0 — R-254: the last two secrets leave the page source, plus a gate against a fourth
gates / gates (push) Successful in 17s

Site one. app_info.html rendered {{.InitialCreds.Password}} into a hidden span —
a REAL per-install credential, read live out of the running container, in the
response body of every render. The page now carries the non-secret half plus a
boolean; the value comes from POST /apps/<slug>/initial-credentials/reveal, which
RE-READS the container rather than serving a cached copy (caching it in the
handler would put it back in the body one layer in). no-store, CSRF-covered,
logged as an act. Both buttons go through it. A reveal that cannot read the value
SAYS SO rather than returning an empty string that renders as a blank password.

Site two, established before changing. The hidden input is NOT the defect and was
left alone: it fires only pre-deploy, and README §318 documents why the value must
round-trip — the customer notes the generated secrets down and submitting them
back is what makes the saved value the same one they saw. The defect was the
neighbouring READONLY input, which on an ALREADY-DEPLOYED app rendered the secret
into a page with nothing to submit. Fixed by POST /stacks/<name>/auto-field/reveal,
authorised by requiring a type:secret auto-field of that stack. Both directions
pinned.

The premise that this contradicted a repo rule does not hold: the rule is
CONTEXT.md:2070 'Password fields require explicit input — prevents accidental
empty-password deployments', about EMPTINESS. No line in the repo says 'no silent
auto-fill'.

The gate. scripts/secret_in_markup_gate.py, registered in controller_gates.py,
convicts any template expression that names a secret unless allowlisted with a
reason. Its limits are MEASURED and in its docstring: it catches a launder through
a local variable (the assignment names the secret) but is blind to a secret
arriving under a neutral page-data key — verified both ways. That is the shape of
site two, which this gate would NOT have caught. The runtime body assertion covers
all shapes but only 4 of 27 page templates; the other 23 are R-255, filed rather
than glossed. Two nets, different holes, both named.

Correction to v0.207.0's report: HTML comments do NOT ship in the response body
here — html/template strips them, text/template does not. Measured. A red-proof
planting a secret in a comment therefore correctly does not fail.
This commit is contained in:
2026-08-07 21:20:26 +02:00
parent 62998aab4f
commit 27d1165962
12 changed files with 755 additions and 22 deletions
+32 -1
View File
@@ -7,7 +7,38 @@
>
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
Last updated: 2026-08-08 (v0.207.0 — R-249/R-252/R-253: a secret leaves the page source, two refusals learn to say what to do)
Last updated: 2026-08-08 (v0.208.0 — R-254: the last two secrets leave the page source, and a gate against a fourth)
> **2026-08-08 — v0.208.0 (R-254). THE RULE, stated so it outlives this session:**
>
> ### A secret is never in a page's response body. It is fetched by an explicit act, and the act is recorded.
>
> Three instances of one pattern shipped in two days, each found by hand: the retrieval passphrase
> (R-249), an app's real first-login password (R-254 site one), and an already-deployed app's generated
> secret field (R-254 site two). Every one was "hidden" with `display:none`, `hidden`, or
> `type="password"` — **instructions a browser honours when DRAWING and nothing else.** The plaintext
> was in the bytes; a `curl` returned it; caches, history, saved pages and screen-shares had it.
>
> **The shape of the fix, now used three times:** the page carries a BOOLEAN; the value comes from a
> **POST** (so CSRF covers it and it is not re-fetchable from history) with **`Cache-Control:
> no-store`**; the reveal is **LOGGED as an act** — reading a value off markup left no trace anywhere,
> which is why nobody can say whether any of these was ever read. **Per-secret endpoints, never one
> generic "reveal any named secret"** — that would turn three narrow exposures into one lever.
>
> **And the test must assert the RAW RESPONSE BODY.** Every test that asked what the customer *sees*
> passed while the bytes carried the secret. That is precisely how this survived three times.
>
> **What is NOT this defect:** a form must carry what it submits. The pre-deploy hidden input round-trips
> a generated secret deliberately (README §318) so the saved value is the one the customer wrote down.
> The defect there was the neighbouring READONLY input on an already-deployed app, where nothing is
> submitted at all.
>
> **The gate:** `scripts/secret_in_markup_gate.py`. Name-based, all 36 templates, **blind to a secret
> arriving under a neutral page-data key** — measured, not assumed. The complementary runtime
> body-assertion covers 4 of 27 page templates; the other 23 are **R-255**.
>
> **A correction to v0.207.0's report:** it said HTML comments ship in the response body. They do not
> here — `html/template` strips them (`text/template` does not). Measured.
> **2026-08-08 — v0.207.0 (R-249, R-252, R-253). Three things the fifth walk exposed BY PASSING.**
> The walk closed R-201 (both halves) on 2026-08-07; none of the below touches the recovery path it