Files
slp-modular-cms/aidlc-docs/features/gitea-deployment-workflow/construction/plans/u3-security-headers-fd-questions.md
T
SluijsensandClaude Opus 5 357d395629 Designs the security headers and observability units
Records the functional design for the two remaining application units,
before any of their code exists.

Security headers have to come from the application, because relying on
nginx or IIS configuration is exactly what this deployment model rules
out. Strict applies to /admin, /api/v1 and /health; a relaxed policy
applies to the public website, which this repository does not author.

The strict policy needs style-src 'unsafe-inline'. That is not a
shortcut: Radix positions dropdowns and dialogs with inline style
attributes recalculated per click and scroll position, and CSP nonces
apply only to style elements, never to style attributes. No nonce- or
hash-based variant leaves the admin UI working. The exception is bounded
to styles — script-src stays closed, which is where XSS actually lives.

The website's policy is enforcing rather than absent, so every
HTML-serving path carries a CSP and no exception has to be recorded. It
still blocks external script origins, so it remains a real boundary.

HSTS is skipped in development: browsers remember it per host and
localhost is shared with unrelated projects. Every other header applies
locally, so a CSP violation surfaces while developing.

For observability, browser error reports tunnel through the API rather
than going to Sentry directly. Ad blockers block Sentry domains, which
loses errors precisely for the users most likely to have browser
oddities. The tunnel forwards only to the host derived from the
configured DSN — a caller-supplied destination would turn an anonymous
endpoint into a request-forgery primitive.

Two consequences of the chosen options are recorded rather than left
implicit:

Enabling SendDefaultPii attaches request headers, and this application
carries two standing credentials in them. Besides the refreshToken
cookie, X-Master-Api-Key would have been sent to a third party on every
error raised during a master/slave call. The scrub list removes the
whole Cookie header, Authorization, X-Master-Api-Key and the request
body.

Console logging at Information plus structured logging to Sentry would,
taken literally, mean one Sentry event per request — exhausting the free
plan within hours and burying real errors in request noise. The
thresholds are split: console keeps Information, Sentry takes warnings
and above as events with Information as breadcrumbs, so every event
arrives carrying the trail that led to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-28 00:01:04 +02:00

8.2 KiB

Functional Design Questions — U3 HTTP Security Headers & CSP

Vul je keuze in achter elke [Answer]:-tag. Kies de laatste optie (Anders) als niets past.


Question 1 — De admin-SPA gaat kapot van een echt strikte CSP

Context: de admin-SPA gebruikt Radix UI-componenten (dialogen, dropdowns, selects) via shadcn-stijl wrappers. Die plaatsen positionering als inline style-attributen op elementen — dat is hoe een dropdown weet waar hij moet staan.

Een CSP met style-src 'self' en zonder 'unsafe-inline' blokkeert die inline styles. Het gevolg is niet een foutmelding maar verkeerd gepositioneerde of onzichtbare menu's en dialogen: kapot op een manier die je pas in de browser ziet, en pas op de plekken waar je klikt.

Er zijn drie manieren om hiermee om te gaan:

A) style-src 'self' 'unsafe-inline' in de strikte policy, met een gedocumenteerde onderbouwing waarom die uitzondering nodig is — pragmatisch en meteen werkend. script-src blijft wél strikt, en dat is waar XSS-risico echt zit B) Nonces of hashes gebruiken voor styles — theoretisch netter, maar Radix genereert styles op runtime per interactie, dus dit werkt in de praktijk niet zonder de componenten te herschrijven C) Begin met Content-Security-Policy-Report-Only voor /admin, kijk wat er daadwerkelijk overtreden wordt, en zet hem daarna afdwingend — niets breekt stil, maar de bescherming staat er in de tussentijd niet X) Anders (beschrijf hieronder na de Answer:-tag)


Question 2 — Wat mag de publieke website?

Context: je koos een ruimere CSP voor de publieke website (D-31), omdat een website-bouwer niet beperkt moet worden door een policy die hij nooit gezien heeft. Maar "ruimer" moet nog wel iets betekenen — de website wordt door hetzelfde proces geserveerd als de admin-UI en de API.

A) Ruim maar niet leeg: sta scripts, styles, afbeeldingen en fonts van eigen origin plus inline toe, en verbindingen naar eigen origin plus de geconfigureerde origins. Externe scripts (bijv. een YouTube-embed) worden dan geblokkeerd tenzij toegevoegd B) Alleen de echt beschermende headers voor de website (X-Content-Type-Options, HSTS, Referrer-Policy) en geen CSP — dan kan een website-bouwer nooit verrast worden C) Ruim, plus in het website-contract vastleggen dat een bouwer extra origins kan laten toevoegen aan de configuratie als hij externe bronnen nodig heeft X) Anders (beschrijf hieronder na de Answer:-tag)


Question 3 — Gelden de headers ook lokaal?

Context: Strict-Transport-Security vertelt de browser: gebruik voor dit domein voortaan altijd HTTPS. Browsers onthouden dat per host, langdurig. Stuur je die header op localhost, dan kan dat je lokale ontwikkeling van andere projecten op dezelfde localhost gaan dwarszitten, en het is niet triviaal om weer ongedaan te maken.

De CSP lokaal wél actief hebben is juist nuttig: dan merk je een overtreding tijdens ontwikkelen in plaats van in productie.

A) CSP en de overige headers ook in Development; HSTS alleen buiten Development — je ziet CSP-problemen vroeg, zonder je localhost te vervuilen B) Alle headers in alle omgevingen, inclusief HSTS lokaal C) Geen enkele header in Development — lokaal zo weinig ruis als mogelijk X) Anders (beschrijf hieronder na de Answer:-tag)


Question 4 — Mag /admin in een iframe?

Context: je koos X-Frame-Options: DENY in de requirements (FR-18). Dat betekent dat geen enkele pagina van dit domein in een iframe geplaatst mag worden — ook niet door de site zelf.

Dat is voor de admin-UI precies goed. Maar het geldt dan ook voor de publieke website, en een klant die op zijn eigen site een pagina in een iframe toont (bijvoorbeeld een formulier of een kaart in een eigen iframe), loopt daar tegenaan.

A) DENY voor /admin en /api/v1, SAMEORIGIN voor de publieke website — de admin-UI blijft maximaal beschermd, de website kan zijn eigen pagina's insluiten B) DENY overal, zoals in de requirements — en een klant die iframes nodig heeft, komt dan bij jou terecht C) SAMEORIGIN overal — eenvoudiger, iets minder strikt voor de admin-UI X) Anders (beschrijf hieronder na de Answer:-tag)


Question 5 — Wat als er geen origins geconfigureerd zijn?

Context: de Umami- en Sentry-origins komen uit configuratie en verschillen per omgeving. In een omgeving zonder Sentry of Umami — bijvoorbeeld lokaal, of een instantie zonder analytics — zijn die lijsten leeg.

Een strikte CSP zonder die origins is correcter (minder toegestaan), maar als iemand later Sentry aanzet en de origin vergeet toe te voegen, worden de foutmeldingen stil geblokkeerd: je monitoring lijkt dan te werken maar ontvangt niets.

A) Lege lijsten zijn normaal — de policy wordt dan simpelweg strikter. Bij opstarten een informatieregel loggen welke origins actief zijn, zodat je in het log kunt zien wat is toegestaan B) Lege lijsten zijn normaal, en géén logging — het is een gewone toestand C) Bij opstarten een waarschuwing als er een Sentry-DSN of Umami-website-ID is geconfigureerd maar de bijbehorende origin niet in de CSP staat — vangt precies de fout waarbij monitoring stil niets ontvangt X) Anders (beschrijf hieronder na de Answer:-tag)


Vervolgvragen (ronde 2)


Follow-up Question 1 — Optie B bij vraag 1 kan technisch niet

Je koos bij vraag 1 optie B: nonces of hashes voor styles. Ik moet dat terugleggen, want ik heb die optie te mild beschreven — hij is niet "theoretisch netter maar lastig", hij werkt principieel niet voor dit probleem.

Waarom niet: een CSP-nonce werkt alleen op <style>- en <script>-elementen. Inline style="..."-attributen vallen daar volledig buiten; een nonce kan er niet aan gehangen worden. Dat is geen implementatiedetail maar hoe de specificatie werkt.

En juist die attributen zijn wat Radix gebruikt: een dropdown die opengaat krijgt zijn positie als style="transform: translate(...)" op het element. Die waarde verandht per klik, per schermgrootte en per scrollpositie.

De enige CSP-mechanismen die inline style-attributen kunnen toestaan zijn:

  • 'unsafe-inline' — staat ze allemaal toe
  • 'unsafe-hashes' met een hash per exacte attribuutwaarde — en die waarden zijn hier dynamisch, dus er is geen eindige verzameling om te hashen

Er is dus geen variant van B die de admin-UI heel laat. Wat overblijft:

A) style-src 'self' 'unsafe-inline' in de strikte policy, met gedocumenteerde onderbouwing. script-src blijft strikt zonder unsafe-inline en zonder unsafe-eval — en daar zit het werkelijke XSS-risico. Dit is wat vrijwel elke applicatie met een component-bibliotheek doet C) Beginnen met Content-Security-Policy-Report-Only voor /admin, kijken wat er in de praktijk overtreden wordt, en daarna afdwingend zetten met de dan blijkbaar benodigde uitzonderingen. Niets breekt stil, maar de CSP beschermt /admin in de tussentijd niet D) Radix vervangen door componenten zonder inline styles — een herbouw van de hele admin-UI, ver buiten de scope van deze feature X) Anders (beschrijf hieronder na de Answer:-tag)


Follow-up Question 2 — Bevestiging: geen CSP op de publieke website is een afwijking

Bij vraag 2 koos je B: alleen de beschermende headers voor de publieke website en geen CSP. Dat is een verdedigbare keuze en past bij je uitgangspunt dat een website-bouwer niet mag struikelen over een policy die hij nooit gezien heeft.

Wel moet ik het expliciet vastleggen, want SECURITY-04 vraagt een CSP op alle HTML-servende endpoints. Zonder CSP op de website betekent dat: als daar ooit een XSS-gat zit, is er geen tweede verdedigingslaag die het beperkt. /admin en /api/v1 houden hun strikte policy, dus het beheerdeel blijft wel beschermd.

Hoe wil je dit vastleggen?

A) Als gedocumenteerde afwijking (DEV-06) met deze onderbouwing, en in het website-contract opnemen dat een bouwer zelf verantwoordelijk is voor uitvoer-escaping op zijn site B) Toch een CSP op de website, maar alleen rapporterend (Report-Only) — dan beperkt hij niets en breekt er niets, maar je ziet wel wat er zou zijn geblokkeerd C) Toch een afdwingende ruime CSP op de website (optie A van de oorspronkelijke vraag), zodat SECURITY-04 gewoon gehaald wordt X) Anders (beschrijf hieronder na de Answer:-tag)