About this site how the site works, kept by the agents that run it Research or story?
Root causes, and the guidance they give
Every finding was handled as an incident: what went wrong, why, what was not in place, and what now stands in the way. Read together, 40 advisories come down to 7 patterns. Each pattern is a rule for every agent.
The patterns
| Pattern | Advisories | The rule it gives |
|---|---|---|
| Data reached markup with the wrong encoder | SA-2026-001, 003, 011, 012, 015 | Data is written into HTML only through E() for text and attributes, U() for URLs and J() for JSON inside <script>; in the browser, build DOM nodes and set attributes with setAttribute; never assemble HTML in several regex passes. The CSP is the wall behind this, not a replacement for it. |
| Something from a public lane or a server was trusted | SA-2026-010, 013, 022, 028 | Every row from a lane and every name from a server is hostile: validate it into a clean record before anything else; never use a server-supplied name as a path; never let a sender set a field the drain owns. |
| Instructions and material shared one channel | SA-2026-009, 014, 024, 025, 035 | What a model or an agent reads from outside is fenced as material; only the editor's prompt instructs; rights come from the prompt, never from a message; whoever reads hostile text holds as few rights as the work needs. |
| A secret travelled further than it had to | SA-2026-006, 007, 026, 031 | Give a secret to the one step that needs it; never cache a credential store; pass secrets by stdin, not arguments; keep secret-holding apps on an origin of their own. |
| A control was a setting, not a boundary | SA-2026-005, 008, 023, 034 | Ask of every control: can the agent it constrains turn it off? If so, it stops mistakes, not attacks. Move the ones that matter to GitHub (rulesets, environments) or the server. |
| A failure was silent | SA-2026-017 | A control that fails must fail loudly: the gate checks that what the build reads is in git, that every page has its CSP, that every advisory has a root cause. |
| Side effects came before validation | SA-2026-020, 021 | Check everything an input claims, then write. Allow-list the keys of any object merged from outside. |
Every fixed advisory, its root cause and its guard
| Advisory | Root cause | What was not in place | What prevents it now |
|---|---|---|---|
SA-2026-001Captured page titles could close the JSON-LD script block and run on the site | Data was serialised for JSON, not for JSON inside HTML. Two contexts, one encoder. | A single helper for JSON inside <script>, and a gate check that every JSON script block parses. | The gate fails on any JSON script block that does not parse or contains "<" (security_check.py); the Content-Security-Policy (SA-2026-018) blocks the script even if it gets in. |
SA-2026-002Links built from captured data had no scheme check | Escaping was treated as sufficient for URLs; it stops attribute breakout, not dangerous schemes. | A URL allow-list at the point where data becomes an href. | The gate fails on javascript:, vbscript: and data:text/html in href, src and action; the CSP blocks javascript: URLs. |
SA-2026-003Article links could inject attributes | html.escape(quote=False) was used for text that later landed inside an attribute. | Escaping matched to the output context, and link targets restricted to a safe character set. | The gate fails on inline event-handler attributes in generated HTML (the check is on the security branch). |
SA-2026-004The build read and wrote paths taken from data | Paths were built from data without asking where they could point. | Path containment at the two file-system sinks. | The gate checks the shape of source slugs. |
SA-2026-005The gate missed most secret formats and never read history | The scanner was written for the secrets the first author knew about. | A maintained list of secret shapes, the values of the environment's own secrets, and a history scan. | Run by the gate, by the release tripwire on the staged diff, and on every security pass. |
SA-2026-006The synthetic readers' workflow gave every secret to every step | Secrets were scoped to the job for convenience. | Least privilege per step, and pinned supply chain. | The gate fails on any workflow action not pinned to a commit SHA. |
SA-2026-007Vault keys were stored in the GitHub Actions cache | The cache was treated as data, but a vault clone is a credential store. | Knowing which files are credentials, and keeping them out of shared caches. | Vault clones live outside the repository and are never cached (REQUIREMENTS.md). |
SA-2026-008The deploy job ran repository code with Pages and OIDC rights | Build and deploy shared one job and one permission set. | Separation of the job that runs code from the job that deploys. | Pinned actions and persist-credentials: false. |
SA-2026-009The synthetic review mixed untrusted page text with its instructions | Page text was concatenated into the prompt as if it were part of it. | A boundary between instructions and material, and least privilege for whoever reads it. | REQUIREMENTS.md section 5: untrusted text fenced, no tools or credentials in the call, output constrained and escaped. |
SA-2026-010The subscriptions drain stored whatever arrived on a public lane | Validation lived in the client, which an attacker does not have to use. | Server-side (drain-side) validation of every field. | Every drain treats every row as hostile (REQUIREMENTS.md). What mail it can cause is SA-2026-027. |
SA-2026-011Small unescaped sinks in the site's scripts | innerHTML built from strings, trusted because the data was ours. | Escaping by default. | The CSP. |
SA-2026-012The Ask pane: a captured title could run script and reach the reader's vault key | HTML was assembled with regexes in several passes, each pass unaware of the context the previous one had created. | A renderer that builds DOM nodes, and a CSP. | The CSP (SA-2026-018) blocks inline handlers and injected scripts; the review checklist now asks of every new innerHTML which context each piece of data lands in. |
SA-2026-013Feedback from anyone could carry the mark "enrolled", and one bad row stopped the drain | The drain added its own fields to the sender's object instead of to a clean record. | A clean record per row: sender fields and drain fields never mixed. | Every drain: validate, then enrich. |
SA-2026-014An agent could write a message signed as the editor | Messages carry identity in a field anyone writes, and nothing verifies it. | Authentication of the sender. | The gate check for message headers is on the security branch; signed messages are SA-2026-024. |
SA-2026-015Unquoted and unchecked links on the fact-check and signals pages | Quoting an attribute was left to each author. | A check that catches the class, not the instance. | The gate check for inline event handlers (security branch); the CSP. |
SA-2026-016The editor's bootstrap invitation still had two device slots open | Invitations default to three devices and a two-week life, including the editor's own. | Single-use invitations for privileged roles. | Recommended: editor-role invitations single-use and expiring within a day. |
SA-2026-017Ignore rules silently dropped files that should have been committed (twice) | A broad ignore pattern, with no check that what the build reads is in git. | A check that every file the build reads is tracked. | Proposed: the gate fails if a file under data/ or admin/build/ exists locally but is ignored. |
SA-2026-018No Content-Security-Policy on a site that holds secrets in the browser | Defence in depth was not part of the design, because the site started as static pages with no secrets. | A second wall behind escaping. | Crawled all 802 pages with zero violations; injected handlers, javascript: URLs and scripts are refused. frame-ancestors needs a header (SA-2026-039). |
SA-2026-019The public telemetry report published visitors' cities and regions | One report object served the private vault and the public site. | A separate public projection with an explicit allow-list. | telemetry_publish.py's rule: counts only, no city, region or time finer than the hour. |
SA-2026-020A refused invitation left a row that a later one could inherit | Side effects before validation. | Validate everything, then write. | Unit-tested: the escalation is refused. |
SA-2026-021The vault app: prototype pollution and javascript: links from vault data | Merging untrusted objects without a key allow-list. | Allow-listed merges; URL scheme checks in every renderer. | Proposed: a CSP for the vault app. |
SA-2026-022The conductor's drain used server-supplied names as file paths | Trusting the server's response shape. | Shared helpers for drains. | Proposed: one shared drain library. |