Table of Contents
Provenance: security:headers
Working log behind headers. Corpus-wide caveats are on corpus. Citations use the shared bibliography.
Run: 2026-08-27. Corpus: 5,859 extracted papers, 7 venues, 2010–2026, data/extract/run1. Item: drain wiki-measuretheweb / “security:headers (new)”, claimed as cursor-drain-headers, store id 221, run 59. Author of the page and this log: Cursor, not Claude Code. Reviews: three focused passes then one generic, all on GPT 5.6 Luna medium (gpt-5.6-luna-medium), the sitting's requested substitute for the spec's sonnet/fable split.
Creating, not extending. node scripts/dw.mjs info security:headers — page did not exist at claim time. ?do=export_raw on a missing page returns the HTML error page (not an existence test by byte count). dw.mjs pages does not list provenance:. Neighbour security already linked the red child. Overlap judgement: write the promised child rather than widening javascript (script XSS) or web_vulnerabilities (exploitability). Those pages keep their own jobs; this page owns the headers a crawl can see.
No ~~DISCUSSION~~ on this provenance page. Comments belong on the content page.
Scope decisions
| Decision | Why | What a reasonable person might have done instead |
|---|---|---|
| Page N = 44 measured, not 176 / 317 / schema-header tuples | 176 is the namespace probe ∩ web. 26 homographs, 13 citation-only, 72 mentions. Schema detection.phenomenon matching header tokens is not usable (nel matched channel). | Publish 176 as “header papers”. That is a keyword hit, not a census. |
| Membership is the namespace regex, not a new one | The parent page already published 317 / 176 as an upper bound. Changing the probe would silently fork the child from the parent. | A tighter regex that drops SRI. Would hide the homograph lesson the parent already flagged. |
| ROLE is a hand map of all 176, residue 0 both ways | The report exits 1 if the sweep and the map diverge. Deciding quotes: 176/176 in .cols (whitespace-normalised). | Sample 30 and extrapolate. Forbidden. |
Let’s Encrypt Certbot CCS 2019 is mention | A lab usability study, not a live-web header census. Recoded after a first-pass agent mapped it measured. | Count every paper that set a header in a lab. That is not this page's question. |
Sicilian CCS 2015 is measured | Three-month SRI churn crawl, then they propose SICILIAN. The measurement precedes the defense. | Map it defense-only and lose the churn figure Steffens later made causal. |
Agarwal CCS 2022 is measured | Counts live-site CSP/XFO/HSTS that extensions inject/drop/rewrite. The subject is the extension's effect, but the figure is a header count on live sites. | Map it mention because the site did not choose the rewrite. The page says what the subject is. |
| Do not census X-XSS-Protection / Expect-CT / HPKP in 2026 | Chromium http_security_headers.cc on main (fetched 2026-08-27) has no Expect-CT and no Public-Key-Pins. Four of 44 still name X-XSS-Protection. Historical rows. | Publish those 4/7/1 as “still measured”. They are named, not current instruments. |
| Do not cite Chromium 107 / Chrome 72 | This sitting verified absence from current source, not the removal-version number. chromestatus JSON 404'd. | Cite training-data version numbers. Rejected. |
observe_headers.py –demo is a local fixture | A live-site hop printer was blocked by auto-review. The lesson is the split (HSTS on the 302, CSP on /app), which a fixture can show. | Pretend we crawled example.com. Forbidden. |
| Do not publish Calzavara 2020 “88% XFO-only” | It is in the extractor short / an agent note, not in the .cols prevalence sentence we could locate. The page uses 1,800 policies from 1,779 origins, which is in .cols. | Repeat the short. Would be a fabricated rate. |
Report script
scripts/header_fold.mjs (SWEEP_RE, ROLE, ROLES) plus scripts/report_headers.mjs. Deterministic. Re-run:
node scripts/report_headers.mjs > scripts/report_headers-output.txt node scripts/verify_headers_figures.mjs bash scripts/external_checks_headers.sh
Flags: --wiki, --list, --quotes, --hits <re>. Unknown flags print FAILURE and exit 1. Exits 1 if corpus size is not 5,859, web not 1,622, crawled not 1,120, missing .cols not 4, web sweep not 176, measured not 44, or a deciding quote is not in .cols. A printed FAILURE with exit 0 is forbidden.
python3 pages/observe_headers.py –demo is the published script. Advertised flags: a URL, or --demo. Both exist on the live object. Demo binds 127.0.0.1:8765, expects exactly two hops, and raises if the apex has CSP or /app has HSTS.
bash scripts/external_checks_headers.sh re-fetches CSP3, Trusted Types, hstspreload.org, RFC 6797, Reporting-1, Chromium http_security_headers.cc, and runs the demo. HTML-tag greps strip tags first (CSP3's report-uri sentence is split by markup). Exits 1 if any check is FAILED. Honours no extra tokens. chromestatus was attempted and 404'd — not a check, recorded as rejected.
scripts/verify_headers_figures.mjs re-checks every per-paper literal on the content page against .cols, and asserts the published <code> / <file> blocks match out/headers/observe_demo.txt and pages/observe_headers.py byte-for-byte.
Queries
| # | Query | Population / denominator | Result | On the page? |
|---|---|---|---|---|
| Q1 | Extraction records | all | 5,859 | outer frame |
| Q2 | crawled | 5,859 | 1,120 | methodology |
| Q3 | web platform | 5,859 | 1,622 | outer frame |
| Q4 | missing paper.cols.txt | 5,859 | 4 | methodology |
| Q5 | namespace SWEEP_RE all platforms | 5,859 | 317 (miss=4) | yes, as upper bound |
| Q6 | Q5 ∩ web | 1,622 web | 176 | yes, as upper bound |
| Q7 | Q5 ∩ crawled | 1,120 | 112 | report |
| Q8 | ROLE hand map size | 176 | 176; residue 0 both ways | yes |
| Q9 | role=measured | 176 | 44 (25.0% of 176; 2.7% of 1,622). 39 also crawled (3.5% of 1,120); 5 measured were not crawled | yes, the N |
| Q10 | measured ∩ crawled | 44 | 39 (5 not crawled) | yes |
| Q11 | homograph / citation / mention / defense / instrument | 176 | 26 / 13 / 72 / 14 / 7 | yes |
| Q12 | posters in 44 | 44 | 0 | yes |
| Q13 | interactionDepth landing-page-only among 39 | 39 | 11 (28.2%) | yes |
| Q14 | headless stated among 39 | 39 | 7 (17.9%); unstated 32 | yes |
| Q15 | statefulness stated among 39 | 39 | 11 (28.2%) | yes |
| Q16 | Reporting-Endpoints in 44 | 44 | 0 | yes |
| Q17 | Reporting-Endpoints in 1,622 web | 1,622 | 0 | yes |
| Q18 | Trusted Types / COOP / COEP named in 44 | 44 | 3 / 3 / 3 | yes |
| Q19 | same tokens in 1,622 web | 1,622 | 13 / 17 / 8 | yes, as “do not pad” |
| Q20 | header-ish detection[] on 44 | 44 | 30 of 44 | report, not a population |
| Q21 | schema phenomenon matching header tokens | all | rejected (nel/channel) | provenance only |
The five measured papers not in the crawled population: Weichselbaum CCS 2016, Mission Accomplished IMC 2017, Long Way to the Top IMC 2018, Familiar Strangers CCS 2020, Chapuis WWW 2020. Felt USENIX 2017 is crawled; its crawlConfig is null (the 1 of 39).
Folding and residue
Membership = platforms.includes('web') AND SWEEP_RE on paper.cols.txt.
SWEEP_RE (same family as the namespace page, written once in header_fold.mjs):
/\bContent-Security-Policy\b|\bCSP\b.{0,30}(header|directive|policy)|strict-transport-security|\bHSTS\b|X-Frame-Options|Subresource Integrity|\bSRI\b/i
ROLE is a hand classification, not a regex fold. Every one of the 176 has a deciding sentence stored in ROLE[key].quote. The report re-checks all 176 against .cols on every run.
Homographs (complete, 26): SRI International / csl.sri.com, Sri Lanka, given name Sri, SRILM, “SRi” as i-th security requirement, “SRI” as security-related information. Listed in report section 10 so a later sitting cannot re-add them.
Rejected probe: publishing the 13 web-platform Trusted Types keyword hits as a census. Three of 44 measured papers name the token; that is the published figure.
Rejected probe: detection.phenomenon matching header-ish tokens as the population.
Quotes spot-checked
ROLE deciding quotes vs paper.cols.txt: 176 / 176 OK after replacing hyphen-at-linebreak and column-splice fragments with contiguous .cols strings. report_headers.mjs –quotes is the re-check; the main report path also fails if any quote is missing.
Content-page per-paper literals: verify_headers_figures.mjs, 44 checked, 0 missing. Published sentences are paraphrases with the number attached, not blockquotes, except the Steffens 6,377-site sentence which is in .cols.
Soni et al. “at an impractical rate”: in .cols as “signatures require signature updates at an impractical rate.”
Not published, because we could not locate it in .cols: Calzavara 2020 “88% still XFO-only” (extractor short only).
External sources
Fetched 2026-08-27, re-checked by scripts/external_checks_headers.sh:
| Source | Load-bearing fact | Verification | Decision |
|---|---|---|---|
| https://www.w3.org/TR/CSP3/ | Working Draft; Last-Modified 13 Aug 2026; report-uri deprecated in favour of report-to; strict-dynamic still defined | HTTP 200; tag-stripped text (the deprecate sentence is split by HTML tags) | Used |
| https://www.w3.org/TR/trusted-types/ | WD 23 June 2026; require-trusted-types-for still defined | HTTP 200 | Used |
| https://hstspreload.org/ | “HSTS preloading is not recommended”; submission max-age>=31536000 + includeSubDomains + preload; points at RFC 6797 | HTTP 200, tag-stripped | Used |
| https://datatracker.ietf.org/doc/html/rfc6797 | still the HSTS spec | HTTP 200 | Used |
| https://www.w3.org/TR/reporting-1/ | mentions Reporting-Endpoints | HTTP 200 | Used |
Chromium net/http/http_security_headers.cc on main (googlesource ?format=TEXT) | no Expect-CT; no Public-Key-Pins; still parses STS | base64 decode of TEXT | Used as current-tree absence, not as a removal-version |
| chromestatus JSON | 404 | 404 | Rejected as a source this sitting |
observe_headers.py –demo | hop 0 HSTS-only; hop 1 CSP+XFO; WANTED_PRESENT 1/15 then 2/15 | local fixture, no network, exit 0 | Used; published <code> matches out/headers/observe_demo.txt |
Rejected: MDN textbook pages as a measurement source; SEO “security headers to add in 2026” listicles; citing Chrome 72 / Chromium 107 from memory.
What could not be established
- A live-web Trusted Types adoption rate after 2024 in these seven venues. Three measured papers name the token; none is a census.
- Any paper in the 1,622 web-platform set that names
Reporting-Endpoints. Zero. - The Chromium milestone that removed Expect-CT / HPKP, from a primary source this sitting (chromestatus 404; source file has no version comment we used).
- Calzavara 2020's “88% XFO-only” rate in
.cols. - A live-site run of
observe_headers.py(auto-review blocked network fetches). The fixture is the published demonstration.
Published script
pages/observe_headers.py. Stdlib. Does not follow redirects: HTTPRedirectHandler.redirect_request returns None, so each hop is an HTTPError the caller prints. Direct header access (got[“location”] on a redirect). Demo raises if hop count ≠ 2 or the split is missing.
Bibliography keys added
Collision-checked against a fresh export of live literature:bibliography at publish time (not the stale pages/literature_bibliography.txt).
Already live, reused: weichselbaum2016_dead, roth2020complex, steffens2021_blockparty, rautenstrauch2024_auth, khodayari2024_great, soni2015_sicilian, hantke2023you, roth2022_security, felt2017_https.
Do not add hantke2023_call (collides with live hantke2023you) or roth2022_lottery (collides with live roth2022_security).
Added this sitting: kranch2015_upgrading, som2017_content, kumar2017_security, chang2017_security, chapuis2020_integrity, calzavara2021_reining, lim2025_phishers, agarwal2022_helping, luo2019_time, zheng2015_cookies, calzavara2020_tale.
USENIX authors for Zheng: patched from citation_author meta (seven authors). fetch_authors.py leaked affiliations. NDSS entries may lack DOI (venue page); filled from the landing URL if Crossref had none.
Report output (unedited)
The file scripts/report_headers-output.txt is the unedited stdout of node scripts/report_headers.mjs from this sitting. Do not edit it by hand; re-run the script.
Reviews
Drafts frozen to out/freeze_headers/ before any reviewer ran. Three focused passes then one generic, all gpt-5.6-luna-medium. The content page was not edited while a pass was running. The content page had no focused-pass edits; bibliography-only fixes (below) were applied before the generic pass.
Focused pass 1 — figures vs script ([Review](e515242a-54a8-4e10-8247-3c6a4c8e6587))
- No findings. Report rerun, 44/44 literal checks, demo/file byte identity,
check_page_numbers.mjswhole-page and–code, unknown-flag exit 1, measured N=44 throughout.
Focused pass 2 — citations and quotes ([Review](c7957de9-98f7-487d-94aa-c5474e86ec06))
- Accept (blocking):
out/headers/bib_new.txtstill contained colliding keyshantke2023_callandroth2022_lottery. Applied: they are not inout/headers/bib_to_append.txt; live keyshantke2023you/roth2022_securityare reused. The content page already cited the live keys. - Accept (lesser):
luo2019_timetitle was the slug. Applied: replaced with “Time Does Not Heal All Wounds: A Longitudinal Analysis of Security Mechanism Support in Mobile Browsers”. - Spot-check of 20 citekeys, 8 deciding quotes, table first-authors, and USENIX author lists: no further findings.
Focused pass 3 — external currency ([Review](f6fb8f81-386b-4708-bdde-98233848d35e))
- No findings. Re-fetched CSP3 (Last-Modified 13 Aug 2026, still WD,
report-uristill deprecated), Trusted Types 23 June 2026, hstspreload.org “not recommended” + 31536000, RFC 6797, Reporting-1, Chromiumhttp_security_headers.cconmain(no Expect-CT, no Public-Key-Pins). Page does not cite Chrome 72 / Chromium 107. External URLs were not hard-404s (ACM DOI 403 is a login wall, not gone).
Generic pass ([Review](f97e56df-6f0a-44b8-a681-92e4b145d322))
- Accept (blocking): Demo could be read as effective HSTS over HTTP. Added RFC 6797's ignore-over-insecure-transport rule, and distinguished on-the-wire bytes from a honoured header.
- Accept (blocking): “44 is 3.9% of 1,120 crawled papers” used the 44 as if they were a subset of crawled. Now: 39/1,120 = 3.5%; five measured papers were not crawled. Report line changed to
measured∩crawled / crawled. - Accept (lesser): “Every claim below is about those seven venues” now “Every corpus-derived claim…”.
- Accept (lesser): “urllib (and every crawler library)” → urllib, and many clients unless configured otherwise.
- Accept (lesser): Khodayari
connect-srcungrouped from “cross-origin isolation headers”. - Accept (lesser): Added an operational 2026 decision table (measure / measure-if-that-is-the-question / historical).
- Accept (lesser):
–hitsexited 0 even if the sweep/map had already setfailed. Nowprocess.exit(failed ? 1 : 0). Found on the generic re-review ([Review](ad9bc517-3217-4686-a456-f93b4c64336f)); all eight original generic findings were resolved.
