Skip to content

feat(security): Subresource Integrity across all scripts + Integrity-Policy (report-only)#74

Merged
jdevalk merged 5 commits into
mainfrom
standards-scan/integrity-policy-2026-07-02
Jul 2, 2026
Merged

feat(security): Subresource Integrity across all scripts + Integrity-Policy (report-only)#74
jdevalk merged 5 commits into
mainfrom
standards-scan/integrity-policy-2026-07-02

Conversation

@jdevalk

@jdevalk jdevalk commented Jul 2, 2026

Copy link
Copy Markdown
Owner

What

Started as "document the Integrity-Policy header on the SRI page" and grew into shipping the whole thing as a worked example, report-only (not enforcing yet).

Docs

  • Expands the Subresource Integrity page with the Integrity-Policy / Integrity-Policy-Report-Only headers, the crossorigin requirement, and a "this site ships it" callout. Adds SRI Level 2 §3.8 + MDN sources. Includes a changed changelog entry.

The site now ships it

  • Self-hosted, pinned Plausible. Frozen copy at /js/plausible.js with a build-time SRI hash; events still POST to plausible.io via data-api, so plausible.io drops out of script-src. A daily GitHub Action re-downloads it and opens a PR on change.
  • Every first-party script carries integrity + crossorigin="anonymous" — committed /public scripts (hashed at build from their bytes), the generated webmcp.js (hashed from its exact bytes via a shared memoised generator), pagefind-component-ui.js (version-stable constant), and admin-stats.js (edge-Function literal).
  • pagefind.js pinned via an inline import map with an integrity entry — the only way to give a dynamically import()ed module integrity metadata; its sha256 is CSP-allowlisted.
  • Integrity-Policy-Report-Only: blocked-destinations=(script) routed to /reports via a new integrity-endpoint. A regression tripwire: a report means a script shipped without integrity.
  • scripts/check-integrity.mjs fails the build if any hand-pinned hash drifts (e.g. a Pagefind bump).

Not enforcing yet. Report-only earned its keep — it surfaced that a valid integrity hash isn't enough: Integrity-Policy also blocks no-cors requests, so classic <script src> needs crossorigin too. Fixed here. Flip Report-Only → enforcing in a follow-up once the production /reports stream confirms a clean run across Firefox/Safari.

Verified

Build + check-integrity green; CI green. Live browser E2E on the preview deploy against the real production headers:

  • Plausible executes under SRI (window.plausible present); plausible.io only in connect-src.
  • Search works — 94 results on /search/, 26 via the ⌘K overlay — i.e. pagefind.js loads under import-map integrity and the injected component-ui.js loads under SRI.
  • Every script has integrity + crossorigin; a ReportingObserver sees 0 integrity-violations on normal pages and through the ⌘K flow.

🤖 Generated with Claude Code

The W3C SRI Level 2 draft's Integrity-Policy / Integrity-Policy-Report-Only
response headers reached cross-engine interoperability (Chrome/Edge 138,
Firefox 145, Safari 26) in late 2025. They enforce SRI site-wide — blocking
any script that loads without integrity metadata — and report violations via
the Reporting API this site already runs. Added an enforcement section, the
SRI Level 2 + MDN Integrity-Policy sources, and a reporting-endpoints related
link. Status stays recommended.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploying specification-website with  Cloudflare Pages  Cloudflare Pages

Latest commit: ba9b488
Status: ✅  Deploy successful!
Preview URL: https://dcb4d1aa.specification-website.pages.dev
Branch Preview URL: https://standards-scan-integrity-pol.specification-website.pages.dev

View logs

jdevalk and others added 4 commits July 2, 2026 10:50
…t map

Groundwork toward enforcing Integrity-Policy: blocked-destinations=(script).
Every script the site loads now either carries SRI or is an exempt destination.

Plausible: serve a frozen copy from /js/plausible.js (data-api points events
back at plausible.io, kept in connect-src) with an SRI hash computed at build
from the committed bytes (src/lib/integrity.ts), so plausible.io drops out of
script-src. .github/workflows/refresh-plausible.yml re-downloads it daily and
opens a PR on change; the hash re-derives at build, so the job only touches
the .js file.

pagefind.js: pagefind-component-ui.js pulls it in via dynamic import(), so an
inline <script type=importmap> with an integrity entry is the only way to give
it integrity metadata. The importmap's sha256 is CSP-allowed; the pagefind.js
sha384 is a committed constant (it is version-stable, emitted after the Astro
build). scripts/check-pagefind-integrity.mjs runs at the end of `npm run build`
and fails — printing the values to paste — if either drifts on a pagefind bump.

WASM/index chunks (fetch) and the search Worker (worker destination) are exempt
from blocked-destinations=(script), so no upstream Pagefind change is needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the script inventory so all script-destination loads carry integrity
metadata, ready for Integrity-Policy enforcement. Committed /public scripts
(purify, trusted-types-policy, search, theme-toggle, spec-toc, track-404,
search-page) are hashed at build from their bytes via a memoised sri() helper.
webmcp.js is generated from spec content, so its generator moved to
src/lib/webmcp.ts and both the endpoint and BaseLayout share the memoised body —
the served bytes are the hashed bytes. pagefind-component-ui.js (emitted after
the Astro build) is a committed constant used in the search.astro tag and the
search.js injection, guarded by scripts/check-integrity.mjs (renamed from
check-pagefind-integrity, now covering both Pagefind files + the CSP import map).

Deferred: /admin-stats.js on the Cloudflare-Access-gated /admin/stats dashboard —
report-only will surface it and it'll be pinned before enforcing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every first-party script now carries SRI, so ship Integrity-Policy-Report-Only:
blocked-destinations=(script) as a regression tripwire — reports (to the
existing /reports collector via a new integrity-endpoint) mean a script shipped
without integrity. It fires nothing in normal browsing, so the privacy stance is
unchanged.

Pin /admin-stats.js (served by the /admin/stats edge Function, so hashed as a
literal) and have check-integrity.mjs verify it matches the file. The report
collector now captures the integrity-violation `destination`. Documented the
worked example on the SRI page, with the Firefox-reporting caveat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Report-only surfaced that every classic <script src> fires an integrity
violation despite a valid integrity hash: Integrity-Policy also blocks no-cors
requests, and a classic script without crossorigin is fetched no-cors. Under
enforcement these would all be blocked, breaking the site. Add
crossorigin="anonymous" to every governed <script> (same-origin cors needs no
server change); module scripts were already cors. Documented the requirement on
the SRI page. Caught before enforcing — exactly what the report-only pass is for.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jdevalk jdevalk marked this pull request as ready for review July 2, 2026 20:12
@jdevalk jdevalk changed the title docs(security): document the Integrity-Policy header on the SRI page feat(security): Subresource Integrity across all scripts + Integrity-Policy (report-only) Jul 2, 2026
@jdevalk jdevalk merged commit eafa83d into main Jul 2, 2026
8 checks passed
@jdevalk jdevalk deleted the standards-scan/integrity-policy-2026-07-02 branch July 2, 2026 20:12
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