feat(csp): de-brittle captcha style CSP via style-src-elem nonce #73
Inga granskare
Etiketter
Inga etiketter
area/backups
area/ci
area/control-panel
area/identity
area/infra
area/observability
area/payments
area/security
area/storage
area/web
blocked
needs-info
needs-triage
ready-for-implementation
type
bug
type
chore
type
docs
type
epic
type
feature
type
task
wontfix
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Inget förfallodatum satt.
Beroenden
Inga beroenden satta
Referens
bitborg/bitborg-web!73
Läser in…
Hänvisa till i nytt ärende
Ingen beskrivning angiven.
Ta bort grenen "feat/64-csp"
Borttagning av en gren är permanent. Även om den borttagna grenen kan fortsätta existera en kort tid innan den faktiskt tas bort, kan det INTE ångras i de flesta fall. Vill du fortsätta?
What
De-brittle the Cap captcha's CSP handling and adopt Astro 7.1's finer-grained style directives.
Today
src/lib/cap-widget-csp.tsstring-scraped the minified Cap widget source to compute asha256hash of its shadow-root inline<style>. That is brittle and fails closed: any upstream restyle changes the bytes, the hash no longer matches, and the captcha silently loses its styling.This replaces the hash with a per-request nonce:
signup-form.astroand allowed onstyle-src-elemvia the Astro 7.1 runtime API (Astro.csp.insertStyleResource({ kind: "element", resource: "'nonce-…'" })), scoped to the sign-up page only.window.CAP_CSS_NONCEhook (set in the page script beforeimport("cap-widget")), so the widget renders<style nonce=…>and it is allowed.src/lib/cap-widget-csp.tsis deleted and its call site removed.Why a nonce, not
'unsafe-inline'The issue suggested
'unsafe-inline'on the new style directive. In practice that does not work here, for two reasons verified against the running build:style-src-elem/-attras raw strings insecurity.csp.directives— they must go throughstyleDirective/the runtime API with akind.<style>(the font-face block, server-island styles) and hashes them. When you scope anything tokind: "element", Astro moves those generated hashes ontostyle-src-elem— and per the CSP spec a hash in a directive neutralises'unsafe-inline'in that same directive. So the captcha's<style>would still be blocked.A nonce coexists with hashes (only
'unsafe-inline'is neutralised, never a nonce), sostyle-src-elem 'self' 'nonce-…' 'sha256-…(Astro's)…'allows: Astro's own inline styles (by hash), same-origin stylesheets (by'self'), and the captcha's<style>(by nonce). This is more precise than'unsafe-inline'(only that one<style>, not all inline styles), restyle-proof, and leavesscript-srcuntouched.Nonce exposure in the DOM (
data-cap-css-nonce) is safe: it is per-request random, so static HTML injection can't know the current value, and anything able to read it from the DOM already executes script (script-src already bypassed).Also in this PR
object-src 'none'added tosecurity.csp.directives— this is #65 item L1. Kept here so allastro.config.mjsCSP edits live in one PR and don't conflict with the #65 branch.deferRender: trueon bothglob()content loaders (guides,faq) insrc/content.config.ts— the small build-memory win noted in the issue. Safe: these are SSR (output: "server") so they render on request anyway.Verification
CSP is off in dev, enforced in
build/preview, so verified against the built standalone server (node ./dist/server/entry.mjs):pnpm check-> 0 errors.pnpm lint-> pass.pnpm build-> success, no CSP warnings.pnpm format:check-> clean./signupresponse CSP header containsstyle-src-elem 'self' 'nonce-<hex>' 'sha256-…'andobject-src 'none';script-srcunchanged ('self' 'wasm-unsafe-eval'+ Astro hashes).style-src-elemnonce matches thedata-cap-css-nonceattribute the widget script reads (verified equal). CSP is delivered as a single HTTP header (node adapterstaticHeaders); there is no duplicate<meta>CSP./) getobject-src 'none'but nostyle-src-elem/nonce — the nonce is page-scoped./en/signupalso carries the nonce.Astro.cache.set(false);cache-control: private, no-store), so the per-request nonce is never cached/reused.Closes #64