security(web): lever de beveiligingsheaders mee als web/.htaccess (#849) #857

Merged
brenno merged 1 commit from fix/849-web-security-headers into main 2026-07-25 10:25:34 +00:00
Owner

Sluit #849.

Wat

Een ZAP-scan tegen de live host meldde zeven ontbrekende responseheaders met één oorzaak: de statische host (Apache) stuurde er geen. CSP en Referrer-Policy zaten al als <meta> in de pagina, maar frame-ancestors, HSTS, X-Frame-Options en Permissions-Policy dwingt een browser alléén als échte header af.

web/.htaccess zet die headers; flutter build web kopieert hem mee naar build/web (de WebReleaseBundle-kopieerstap filtert dotfiles niet, en pack_web_release neemt hem via listSync automatisch in SHA256SUMS op). De hardening reist zo mét de bundel in plaats van los in serverconfig te leven.

Bewuste keuzes op de sticky headers

  • HSTS: max-age=63072000; includeSubDomains, zonder preload (de sticky, moeilijk terug te draaien keuze). Sluit aan op wat HOSTING.md §3 en BUILD.md al voorschreven.
  • COEP (require-corp, ZAP 90004): bewust niet meegeleverd — kan cross-origin resources breken en vraagt eerst een test.
  • ZAP-baseline: 10027 ("Suspicious Comments") op IGNORE — de triggerwoorden komen uit de geminificeerde main.dart.js, niet uit commentaar.

Regressiepoort

  • test/web_htaccess_headers_test.dart (draait onder make check): de header-CSP is byte voor byte gelijk aan de meta-CSP, en elke header-only-hardening staat er met de conservatieve waarde.
  • tool/check_web_hardening.dart (op de gebouwde bundel, make check-web): bewijst dat flutter build web de dotfile écht meekopieert en dat de header-CSP de meta-CSP spiegelt.

Poorten

  • make check: groen (6607 tests, coverage 86,6%, per-file floor 0).
  • make check-secrets: schoon (gitleaks: no leaks; trufflehog: 0).
  • make sast (semgrep, lokale regels): 0 findings.
  • DAST niet lokaal gedraaid: docker/ZAP ontbreekt hier, en de .htaccess-headers manifesteren zich pas op een échte Apache-host — een lokale ZAP tegen python3 -m http.server zou ze niet zien. Validatie hoort bij de post-deploy-DAST (#850) tegen de live host, ná uitrol.

Bewaker

Bewust overgeslagen: geen wijziging aan bestandsformaat, opslag, afhankelijkheid of een nieuwe vertrouwde partij. Het is defensieve hardening; de docs (HOSTING/BUILD/CHANGELOG) beschrijven alleen de meegeleverde headers, consistent met de reeds bestaande HOSTING.md §3.

Voorwaarde op de host

.htaccess werkt alleen met mod_headers aan én AllowOverride die FileInfo/All toestaat. Staat AllowOverride uit, dan horen dezelfde regels in de vhost — gedocumenteerd in HOSTING.md §3 en BUILD.md.

Sluit #849. ## Wat Een ZAP-scan tegen de live host meldde zeven ontbrekende responseheaders met één oorzaak: de statische host (Apache) stuurde er geen. CSP en Referrer-Policy zaten al als `<meta>` in de pagina, maar `frame-ancestors`, HSTS, `X-Frame-Options` en `Permissions-Policy` dwingt een browser alléén als échte header af. `web/.htaccess` zet die headers; `flutter build web` kopieert hem mee naar `build/web` (de WebReleaseBundle-kopieerstap filtert dotfiles niet, en `pack_web_release` neemt hem via `listSync` automatisch in SHA256SUMS op). De hardening reist zo mét de bundel in plaats van los in serverconfig te leven. ## Bewuste keuzes op de sticky headers - **HSTS**: `max-age=63072000; includeSubDomains`, **zonder** `preload` (de sticky, moeilijk terug te draaien keuze). Sluit aan op wat HOSTING.md §3 en BUILD.md al voorschreven. - **COEP (`require-corp`, ZAP 90004)**: bewust **niet** meegeleverd — kan cross-origin resources breken en vraagt eerst een test. - **ZAP-baseline**: 10027 ("Suspicious Comments") op IGNORE — de triggerwoorden komen uit de geminificeerde `main.dart.js`, niet uit commentaar. ## Regressiepoort - `test/web_htaccess_headers_test.dart` (draait onder `make check`): de header-CSP is **byte voor byte** gelijk aan de meta-CSP, en elke header-only-hardening staat er met de conservatieve waarde. - `tool/check_web_hardening.dart` (op de gebouwde bundel, `make check-web`): bewijst dat `flutter build web` de dotfile écht meekopieert en dat de header-CSP de meta-CSP spiegelt. ## Poorten - `make check`: **groen** (6607 tests, coverage 86,6%, per-file floor 0). - `make check-secrets`: schoon (gitleaks: no leaks; trufflehog: 0). - `make sast` (semgrep, lokale regels): 0 findings. - **DAST niet lokaal gedraaid**: docker/ZAP ontbreekt hier, en de `.htaccess`-headers manifesteren zich pas op een échte Apache-host — een lokale ZAP tegen `python3 -m http.server` zou ze niet zien. Validatie hoort bij de post-deploy-DAST (#850) tegen de live host, ná uitrol. ## Bewaker Bewust overgeslagen: geen wijziging aan bestandsformaat, opslag, afhankelijkheid of een nieuwe vertrouwde partij. Het is defensieve hardening; de docs (HOSTING/BUILD/CHANGELOG) beschrijven alleen de meegeleverde headers, consistent met de reeds bestaande HOSTING.md §3. ## Voorwaarde op de host `.htaccess` werkt alleen met `mod_headers` aan én `AllowOverride` die `FileInfo`/`All` toestaat. Staat `AllowOverride` uit, dan horen dezelfde regels in de vhost — gedocumenteerd in HOSTING.md §3 en BUILD.md.
security(web): lever de beveiligingsheaders mee als web/.htaccess (#849)
All checks were successful
scans / scans (pull_request) Successful in 3m15s
67d9fb026d
Een ZAP-scan tegen de live host meldde zeven ontbrekende responseheaders met
één oorzaak: de statische host stuurde er geen. De CSP en Referrer-Policy zaten
al als <meta> in de pagina, maar frame-ancestors, HSTS, X-Frame-Options en
Permissions-Policy dwingt een browser alleen als echte header af.

web/.htaccess zet die headers voor een Apache-host; flutter build web kopieert
hem mee naar build/web, dus de hardening reist met de bundel. De CSP-header is
een byte-voor-byte kopie van de meta-CSP, bewaakt door test/web_htaccess_
headers_test.dart (onder make check) en check_web_hardening.dart (op de gebouwde
bundel), zodat de twee niet uiteen kunnen lopen.

Bewust conservatief: HSTS met includeSubDomains maar zonder preload, en COEP
(require-corp) blijft eraf tot een cross-origin-test. ZAP-baseline: 10027 op
IGNORE (woorden uit de geminificeerde main.dart.js, geen commentaar).

Gedocumenteerd in HOSTING.md §3, BUILD.md en CHANGELOG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
brenno merged commit 8c440e0f31 into main 2026-07-25 10:25:34 +00:00
Sign in to join this conversation.
No description provided.