Files
troupe-standup/recoveries/render-site-guards.md
T

16 KiB
Raw Blame History

Q29 — The guards that are not the walk

Screen. Every claim below carries a path, a hash, a URL, or a command with its output, or the word unverified. Nothing secret is printed in this file. Sole author: Merlin. Scratch tree: /root/crucible/q29/ — re-runnable with python3 q29-guards.py, then python3 q29-verify.py, then python3 q29-exclusions.py (logs: Q29-GUARDS.txt, Q29-VERIFY.txt, Q29-EXCLUSIONS.txt, Q29-PROBE.txt; data: Q29-RESULTS.json, Q29-VERIFY.json, Q29-EXCLUSIONS.json).

Run clock: 2026-10-11, ~05:4x–06:1x UTC, this host. Builder under test: the Q27-patched build_blocks_hard.py, sha256 c4127c6c9325ed9f8cab03b80b454cccde0f9f1e768289ff216d2fb79e78dbf2 (29,174 B), copied into the scratch tree and re-verified before any fixture. blocks.json: sha256 63c9cee6103a6824c90c71440fa983d2f5592e380f9c3fb40f7ad366ae41723d (278,459 B), read-only throughout — every fixture mutates a copy. No fixture carries a {{/}} sentinel: the walk kills those (Q26/Q28). These fixtures carry no token at all, so every build that exits 0 exercises the mechanism that protects the emitted byte.


1. The second guarantee, in one sentence

Every string the build emits is either escaped for its context or derived from a value that cannot carry a metacharacter: HTML text and attributes go through esc() (html.escape(v, quote=True), patched build_blocks_hard.py:87), the JSON-LD script payload goes through json.dumps plus a </ → <\/ replacement (:362) so no value can terminate the script element, URLs must be https:// before they are rendered (safe_url, :242) and are escaped in the attribute, and every classified field (status, ledger, publication_status) is validated against a closed map before any label is rendered (:266-271). (Patched-builder numbering throughout the sentence; pristine equivalents in the table below.)

The mechanism inventory. Line numbers below are the builder under test — the patched build_blocks_hard.py (sha256 c4127c6c…) — with the pristine build_blocks.py (/root/stackchain-legends-build/build_blocks.py, sha256 97fedb5dd5e85d2…) equivalent in parentheses where it differs:

mechanism site (patched / pristine) what it guards sufficiency for that context
esc() = html.escape(v, quote=True) :87 (same) every HTML text node and every HTML attribute escapes < > & " ' — complete for both contexts
json.dumps(...).replace("</", "<\\/") :362 / pristine :208 (payload in head()) the <script type="application/ld+json"> payload </script (case-insensitive) is the only sequence that terminates the element, and every </ is escaped
safe_url() scheme check :242 / pristine :96, called :272 body_source_url rejects any non-https:// value before render; esc() then protects the attribute
entry validation vs STATUS_LABEL/LEDGER_LABEL :266-269 / pristine :121-123 badge class + badge text the key is never rendered; unknown key dies pre-write
slug_for() sanitisation :237 / pristine :91 canonical href, sitemap loc, index li id, pager hrefs [^a-z0-9-]+ → -; no metacharacter survives (measured: slug_for("<b>") → b)
sortable[:4] + esc() :545, :549 / pristine :391, :395 index year heading the slice cannot inject a tag; esc() escapes the residue
receipt rows built through esc(k)/esc(v) :417 / pristine :263 the whole receipt table formatted first, escaped after

2. Fixtures — 22 plus the control

Each fixture is its own scratch tree under /root/crucible/q29/fixtures/<name>/ holding a mutated copy of blocks.json and the patched builder. "Files" = files written at exit. "Emitted" = the bytes actually on the page, quoted from Q29-VERIFY.json. All builds ran python3 build_blocks_hard.py in the fixture tree.

# fixture mutation exit files emitted evidence
1 title-metachars magazine-001.title = '<b>hi</b> & "q" \'z < 5' 0 125 <title>&lt;b&gt;hi&lt;/b&gt; &amp; &quot;q&quot; &#x27;z &lt; 5 …; meta … content="&lt;b&gt;hi&lt;/b&gt; &amp; &quot;q&quot; &#x27;z &lt; 5"; h1 same; index label same; JSON-LD "headline": "<b>hi<\/b> & \"q\" 'z < 5 …" (raw <b>, escaped <\/b>)
2 title-closescript title = '</script><b>hi</b>' 0 125 "headline": "<\/script><b>hi<\/b> …"; raw </script in page = 1 (the page's own closer; the payload's is <\/script)
3 title-closescript-upper title = '</SCRIPT><B>' 0 125 "headline": "<\/SCRIPT><B> …"; raw </script in page = 1
4 title-comment-nested title = '<!--<script>alert(1)</script>-->' 0 125 "headline": "<!--<script>alert(1)<\/script>--> …"; raw <!-- in page = 1 (inside the payload only)
5 title-barelt title = '5 < 6 and 7' 0 125 "headline": "5 < 6 and 7 …" (json.dumps keeps <); <title>5 &lt; 6 and 7 …
6 date-closescript magazine-001.date = date_display = '</script><b>x' 0 125 "datePublished": "<\/script><b>x"; HTML Date: &lt;/script&gt;&lt;b&gt;x
7 displayname-metachars contributor.display_name = 'A & B <x> "y" \'z' (no handle) 0 125 byline A &amp; B &lt;x&gt; &quot;y&quot; &#x27;z; JSON-LD "author": {"@type": "Person", "name": "A & B <x> \"y\" 'z"}
8 displayname-closescript display_name = 'Zoe </script><i>' 0 125 "author": {… "name": "Zoe <\/script><i>"}; raw </script in page = 1
9 handle-metachars contributor.handle = 'x" onmouseover=alert(1)' 0 125 byline <a href="https://x.com/x&quot; onmouseover=alert(1)">@x&quot; onmouseover=alert(1)</a>; JSON-LD "url": "https://x.com/x\" onmouseover=alert(1)" (json.dumps escapes the quote)
10 handle-closescript handle = 'a</script>b' 0 125 byline <a href="https://x.com/a&lt;/script&gt;b">@a&lt;/script&gt;b</a>; JSON-LD "url": "https://x.com/a<\/script>b"; raw </script in page = 1
11 panel-closescript plain_summary.text = 'the </script> break' 0 125 panel the &lt;/script&gt; break; raw </script in page = 1
12 provenance-comment body_provenance = '<!-- [archivist note] --> & <b>' 0 125 panel &lt;!-- [archivist note] --&gt; &amp; &lt;b&gt;
13 excerpt-metachars excerpts[0].text = 'they said "5 < 6" & it\'s true' 0 125 blockquote they said &quot;5 &lt; 6&quot; &amp; it&#x27;s true
14 excerpt-url-quote excerpts[0].url = 'https://x.com/a" onmouseover=alert(1)' 0 125 <a href="https://x.com/a&quot; onmouseover=alert(1)" …> (no safe_url on excerpt URLs; esc() still holds the attribute)
15 sourceurl-quote block-6.body_source_url = 'https://x.com/a" onmouseover=alert(1)' 0 125 <cite>Source: <a href="https://x.com/a&quot; onmouseover=alert(1)">…
16 quotebranch-body block-6.body = 'he said "hi" <b> and 5 < 6' (quote branch) 0 125 blockquote he said &quot;hi&quot; &lt;b&gt; and 5 &lt; 6
17 reuse-status block-5.receipt.reuse_status = '<i>yes</i>' 0 125 Image reuse: &lt;i&gt;yes&lt;/i&gt;.
18 receipt-usd-nonnumeric block-5.receipt.usd = '<b>7' 0 125 Amount $&lt;b&gt;7; Arithmetic not checked; Amount in sats 22,765 (the surviving int)
19 receipt-sats-nonnumeric block-5.receipt.sats = 'x' 1 2 stderr Traceback (most recent call last):; the two files written are event-2022-07-18-anthony-enters-at-16-names-the-hill-at-24/index.html and event-2022-07-18-the-5-invitation-becomes-a-chain/index.html — a partial tree (see §5)
20 status-metachar magazine-001.status = '<b>' 1 0 BUILD FAILED: block[30] unknown status '<b>' — validation, not escaping
21 year-derived magazine-001.sort_date = '<b>2023-08-27' 0 125 index year headings end ('&lt;b&gt;2', '&lt;b&gt;2'); li id="magazine-001" unchanged
22 control no mutation 0 125 tree digest 91ecf6bf939a2a499983066a2331bb953eab8a4ca85c66a8a19f42cb1b28f7a8 — byte-identical to the Q26 baseline and to the live site-ledger (diff -rq silent)

Proof-or-falsify results, stated against the emitted byte, not the intent:

  • </script> in title, date, display_name, plain_summary.text: proven safe in all four. Payload landings carry <\/script…; HTML landings carry &lt;/script&gt;. Raw </script (not preceded by \) in each page = 1, the page's own closing tag.
  • <!--: survives raw into the payload ("<!--<script>alert(1)<\/script>-->"), and is inert there: a script element is a raw-text element, so <!-- opens no comment, and every </ sequence inside is escaped — even in the HTML spec's script-data double-escape states nothing can terminate the element.
  • Lone " in an attribute-rendered field: proven safe. title → meta description (&quot;), handle → href and link text (&quot;), excerpts[0].url and body_source_url → href (&quot;). No attribute breakout in any of the four.
  • & in every context: escaped in HTML (&amp;), raw in the payload — and inert there, because raw text performs no entity decoding.
  • < (bare), <script: survive raw into the payload; inert in a raw-text element.

Live-data corroboration (not a fixture). The live pages already exercise esc() on production values: https://alexanderwhitestone.com/stackchain/magazine-012/ (4,095 B, sha256 f6d4d8e50d549684…, fetched this run) serves <title>Inflation &amp; You …</title> and JSON-LD "headline": "Inflation & You …"; magazine-035 (4,543 B, 871638b8abb50137…) carries &#x27; ×4. The escaping is not fixture-only; the live tree already depends on it.


3. Where no mechanism is needed — measured exclusions

# surface measured command output
E1 CSS / FOOT / STATUS_LABEL / LEDGER_LABEL literals grep -cF '.format(' on both builders 0 and 0; grep -n 'ledger.css.*write_text' → 324: (OUT / "ledger.css").write_text(CSS); grep -cF 'parts.append(FOOT)' → 3; constants assigned at :45 (CSS), :35 (STATUS_LABEL)
E2 blocks-build.json (the manifest) json keys of every entry, and of the file every value ['bytes','sha256']; top level ['cleared','files','pages','research_only','schema']; the three counters are int; every manifest key matches [a-z0-9/.-]+
E3 sitemap.xml regex over the control tree 123 <loc>s, every one https://alexanderwhitestone.com/stackchain/<slug>/; slug_for('<b>') → b; write site :336 applies esc(u)
E4 receipt arithmetic's formatted strings grep for the row build 417: body = "".join(f"<tr><th>{esc(k)}</th><td>{esc(v)}</td></tr>" …) — formatted, then escaped; fixtures 18/19 measure the two failure shapes
E5 raw </script over the whole control tree re.findall(r"(?<!\\)</script", …) over 123 pages total 123, per-page values [1] — exactly the legit closer on every page, nothing else
E6 esc() / payload semantics, measured directly html.escape(s, quote=True); json.dumps(s); the replace '<b>'→'&lt;b&gt;', '&'→'&amp;', '"'→'&quot;', "'"→'&#x27;', '</script>'→'&lt;/script&gt;', '<!--'→'&lt;!--'; json.dumps('<b>') → "<b>"; json.dumps('</script>').replace('</','<\\/') → "<\/script>"

4. What this adds to Q26/Q27/Q28

Q26 proved the two silent token classes die; Q27 enumerated the render sites and closed the uncited-handle class pre-write; Q28 accounted for the channels the walk cannot reach and printed the first guarantee. This item is the complement: the mechanisms that guard values with no token. Measured result: no metacharacter in any blocks.json field reaches any emit site unescaped or un-escaped-for-context, in any of the 22 fixtures — the one exception is the JSON-LD payload's raw <, &, <!--, <script, which are inert in a raw-text script element and are the documented, measured behaviour of json.dumps plus the </ replacement.

Q29's item text asked (task 4) to restate two Q27 counts. Q28 already discharged both — 36 esc() call lines / 43 occurrences and the 42-vs-35 fixture breakdown — and I re-measured the first this run (grep: 36 lines carrying esc( excluding the definition at :87, 43 occurrences), reproducing Q28's figure. The item's other premise, "three of Q27's 32 rows carry no esc() call (:208 the payload, :286 datePublished, :405→:406)", also re-verifies against the pristine builder: json.dumps sites are at :192 and :208 (:192 is the manifest, :208 the payload), :286 is ld["datePublished"] = b["date"], and the gaps loop's call is at :406.


5. One finding: an unguarded int() can still leave a partial tree

Fixture 19 (block-5.receipt.sats = 'x') is the one measured hole. receipt_table guards float(price_raw) and float(usd) in a try/except (fixture 18 falls back to not checked), but f"{int(sats):,}" at build_blocks.py:416 has no guard: the ValueError propagates as a traceback, the process exits 1, and the write loop has already emitted 2 files (the two entries that sort before block-5). That contradicts the builder's own docstring — "Fails closed: any schema problem, missing required field, or non-https source URL aborts the whole build rather than emitting a partial tree" — for the type-coercion class. It is the same defect shape Q26/Q27 closed for the token classes (a failure that leaves a half-emitted tree), reached through a different door.

Recommended, not staged: wrap int(sats) in the same try/except shape as the price line (fallback to the raw string, still esc()'d), or validate receipt field types in the pre-write pass beside safe_url. Either is a one-line-class change to build_blocks.py — Alexander's, the same gate Q26/Q27/Q28 state. The two unguarded int(sats) sites are build_blocks.py:258 and :262 (patched builder: :412 and :416), and the guarded float() pair sits at :252/:257 (patched :406/:411). Today's blocks.json carries int sats on both present receipts (n = 5 and 7, measured in Q29-PROBE.txt), so this is latent, not live.


6. Limits

  • No fuzzer. The shapes tested are the enumerated set from the item text (the metacharacter families <, &, ", ', </, <!--, <script, and the receipt type shapes). A random metacharacter fuzz over all 121 entries was not run; the guarantee is stated over the mechanisms, not over an exhaustive input census.
  • The guarantee covers the builder's emit surface, not the deploy pipeline. The pages are merged into the live web root at deploy time by a separate process this build does not touch.
  • Fixture 19's partial tree is measured, its cause is source-read (no try/except around int(sats) at :416); the traceback itself is in Q29-RESULTS.json.
  • The JSON-LD payload's raw </&/<!-- are measured inert, not proven inert by a parser. The reasoning is the HTML raw-text-element rule (only </script terminates, case-insensitively, and every </ is escaped); the emitted bytes are the evidence, the rule is the argument.
  • No X page was fetched this run. The only live fetches are the three ledger pages in §2's corroboration.

7. Nothing touched

  • Shared build tree: build_blocks.py still sha256 97fedb5dd5e85d2b10071e8e4904b3a9b1573072149e722fbece93eaf0774672, blocks.json still 63c9cee6103a6824c90c71440fa983d2f5592e380f9c3fb40f7ad366ae41723d, find /root/stackchain-legends-build -newermt '2026-10-11 04:00' -type f → empty.
  • Live archive: https://alexanderwhitestone.com/stackchain/blocks/ → 200 / 34,488 B / sha256 4f764ef4dfb9c08b2654ff0783550d20ad1bb7a31cb0c32070974a5fe451557e (fetched this run, identical to the Q26/Q27/Q28 readings).
  • blocks.json read-only throughout; decisions/ and inbox/ untouched; nothing executed; no other party's file edited; the patched builder stays scratch-local in /root/crucible/q29/. The publish precondition from Q27/Q28 stands unchanged: adopt c4127c6c into the shared build tree first, then publish — Alexander's call.
  • Scratch tree contents: q29-probe.py, q29-guards.py, q29-verify.py, q29-exclusions.py, build_blocks_hard.py, fixtures/ (23 trees: 22 fixtures + control), the five logs and three JSON data files listed in the screen.

— Merlin, 2026-10-11