Skip to content

refactor(csp): delete the hashed-CSP rollout flag and the branch it guarded - #527

Merged
oddharsh merged 2 commits into
mainfrom
claude/csp-drop-rollout-flag
Aug 24, 2026
Merged

oddharsh merged 2 commits into
mainfrom
claude/csp-drop-rollout-flag

Conversation

@oddharsh

Copy link
Copy Markdown
Owner

ENFORCE_PAGE_HASHES was pinned true on 2026-08-16 and nothing flipped it in either direction after that day, so its false arm has been dead code for a week.

It was never one line. The flag cost a second header name, a second tail constant (CSP_TAIL_REPORTABLE), a tail parameter threaded through both policy builders purely to feed that constant, an Object.entries loop in withSecurityHeaders whose only job was carrying a second header past a bail, and two branches apiece in the contract test and the twin-fact tripwire. Net −110/+92, and most of the insertions are the history note and the strengthened assertions.

The emitted policy is byte-identical

Checked both directions. CSP_TAIL now holds exactly the string CSP_TAIL_REPORTABLE + "; upgrade-insecure-requests" used to compose, and building each side produces the same _headers line:

default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; worker-src 'self'; manifest-src 'self'; upgrade-insecure-requests

cspHeadersFor returns exactly one header, and the test counts keys

The contract test now asserts Object.keys(...) deep-equals ["content-security-policy"] rather than probing one header name, because a twin under any other spelling is the same regression.

The upgrade-insecure-requests assertion inverts with this change, which is the part worth reading. It used to check the twin OMITTED the directive, since a browser ignores a navigation directive in a report-only policy and files an issue per page load. With one policy left, the only way to get it wrong is to lose it, so it now asserts presence on both arms. Asserting the omission is what would have passed just as happily if the directive fell out of everything.

The twin tripwire now reads headers instead of a flag

gen-md-twins derived /security's rollout sentence from ENFORCE_PAGE_HASHES. It now derives it from whether cspHeadersFor emits a report-only header at all.

That is a better check for the reason the flag era itself proved: a flag states an intention, and #249's bug was the enforcing half never being applied to a single document while the flag said it was. /security claims 'unsafe-inline' left script-src; that claim goes false the moment a twin returns, so the deploy fails until one of the two is put right.

Controls, both run

Reintroducing a report-only twin in cspHeadersFor:

  • contract test fails by name, reporting the two keys it found
  • twin-fact tripwire fails by name: src/worker/lib/security.ts no longer states its hashed-CSP enforcement state

Reverting makes both green.

The rollout story stays, elsewhere

What the DevTools sweep found on /garage/horizon and why the enforcing half had never been applied are gotcha 17 in CLAUDE.md, which is where they belong now the mechanism is gone. Rolling back is git revert rather than a flag, and bun run csp:sweep is the signal that would call for one; its header now says that instead of describing a flip that already happened.

Verification

353 tests, typecheck, lint, build, pages:check, photos:check, twin facts, and the 142-route oracle green, re-run after rebasing onto #526.

The build's taste warnings on this branch are the pre-existing ones #525 fixes, not new here.

@github-actions

github-actions Bot commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Wire-size diff vs merge base

0db8fda → 6b1673a

Brotli q11, the quality the edge serves. Every number here is deterministic:
an unchanged file produces no row. Advisory — this check fails on nothing.

Client assets

No change. 22 files, 77.74 KiB Brotli total.

Pages

No change. 50 files, 531.29 KiB Brotli total.

Worker bundle

237.91 KiB → 237.85 KiB gzip (-0.06 KiB, -0.0%)

Largest module deltas (raw bytes in bundle):

file base head Δ
.build/src/worker/lib/security.ts 3.15 KiB 3.00 KiB -0.16 KiB (-4.9%)

Dictionary deltas (pd/)

No change. 146 deltas, 492.24 KiB total.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
aadhar-sh caa7568 Aug 24 2026, 01:20 AM

…uarded

ENFORCE_PAGE_HASHES was pinned true on 2026-08-16 and nothing has flipped it in
either direction since, so its false arm has been dead code for a week. It was
not one line: it cost a second header name, a second tail constant, a `tail`
parameter threaded through both policy builders to feed that constant, an
entries loop in withSecurityHeaders whose only job was carrying a second header
past a bail, and two branches apiece in the contract test and the twin-fact
tripwire.

The emitted policy is byte-identical, checked both ways. CSP_TAIL now holds the
string CSP_TAIL_REPORTABLE plus "; upgrade-insecure-requests" used to compose,
and a build of each side produces the same _headers line.

cspHeadersFor returns exactly ONE header now. The contract test counts the keys
rather than probing one name, since a twin under any other spelling is the same
regression, and the upgrade-insecure-requests assertion inverts with the change:
it used to check the twin OMITTED the directive, and with one policy left the
only way to get it wrong is to lose it, so it now checks presence on both arms.

gen-md-twins no longer reads the flag. It reads the header set cspHeadersFor
actually emits, which is the better check for the reason the flag era proved:
a flag states an intention, and #249's bug was the enforcing half never being
applied to a single document while the flag said it was. /security claims
'unsafe-inline' left script-src, and that claim goes false the moment a
report-only twin returns, so the deploy fails until one of the two is put right.

Controls, both run: reintroducing a report-only twin fails the contract test by
name (it reports the two keys it found) and fails the twin-fact tripwire by name.
Reverting makes both green again.

The rollout story stays. What the DevTools sweep found on /garage/horizon, and
why the enforcing half had never been applied, are gotcha 17 in CLAUDE.md, which
is where it belongs now that the mechanism is gone. Rolling back is `git revert`
rather than a flag, and `bun run csp:sweep` is the signal that would call for
one; its header says so instead of describing a flip that already happened.

353 tests, typecheck, lint, build, pages, photos and the 142-route oracle green.
The build's taste warnings on this branch are the ones #525 fixes, not new.
@oddharsh
oddharsh force-pushed the claude/csp-drop-rollout-flag branch from b26451b to 50b6e01 Compare August 24, 2026 01:18
@oddharsh
oddharsh enabled auto-merge (squash) August 24, 2026 01:19
@oddharsh
oddharsh merged commit c27025f into main Aug 24, 2026
6 checks passed
@oddharsh
oddharsh deleted the claude/csp-drop-rollout-flag branch August 24, 2026 01:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant