diff --git a/api/tests/Feature/Security/CspConnectsToObservabilityTest.php b/api/tests/Feature/Security/CspConnectsToObservabilityTest.php new file mode 100644 index 00000000..eb569899 --- /dev/null +++ b/api/tests/Feature/Security/CspConnectsToObservabilityTest.php @@ -0,0 +1,109 @@ +get(). Caught a real PR-3 regression: frontend SDK + * emitted events correctly, but Crewli's strict CSP blocked egress at + * the browser. Without this guard, the bug would resurface every time + * a CSP refactor happens. + */ +final class CspConnectsToObservabilityTest extends TestCase +{ + private const DEV_GLITCHTIP_HOST = 'http://localhost:8200'; + + private const PROD_GLITCHTIP_HOST = 'https://monitoring.hausdesign.nl'; + + public function test_dev_meta_csp_whitelists_localhost_glitchtip(): void + { + $html = file_get_contents(base_path('../apps/app/index.html')); + $this->assertNotFalse($html, 'apps/app/index.html must be readable'); + + $csp = $this->extractMetaCsp($html); + $this->assertNotNull($csp, 'apps/app/index.html must declare a meta CSP'); + + $connectSrc = $this->extractDirective($csp, 'connect-src'); + $this->assertNotNull($connectSrc, 'meta CSP must define connect-src'); + + $this->assertStringContainsString( + self::DEV_GLITCHTIP_HOST, + $connectSrc, + sprintf( + 'Dev meta CSP connect-src must whitelist %s for the local GlitchTip stack (RFC-WS-7 §3.5). ' + .'Without it the browser blocks @sentry/vue events with: "Refused to connect because it ' + .'violates the following Content Security Policy directive: connect-src ...". ' + .'Found connect-src: %s', + self::DEV_GLITCHTIP_HOST, + $connectSrc, + ), + ); + } + + public function test_prod_nginx_csp_whitelists_monitoring_host(): void + { + $conf = file_get_contents(base_path('../deploy/nginx/csp-spa.conf')); + $this->assertNotFalse($conf, 'deploy/nginx/csp-spa.conf must be readable'); + + $matches = []; + $found = preg_match_all('/^\s*add_header\s+Content-Security-Policy(?:-Report-Only)?\s+"([^"]+)"\s+always;/m', $conf, $matches); + $this->assertNotEmpty($found, 'csp-spa.conf must contain at least one add_header Content-Security-Policy directive'); + + // Every uncommented add_header line must include the GlitchTip host + // in connect-src. Multiple lines exist (Report-Only + enforce); both + // need the whitelist or a refactor that flips between them silently + // breaks observability. + foreach ($matches[1] as $cspValue) { + $connectSrc = $this->extractDirective($cspValue, 'connect-src'); + $this->assertNotNull($connectSrc, "csp-spa.conf CSP must define connect-src. Got: {$cspValue}"); + $this->assertStringContainsString( + self::PROD_GLITCHTIP_HOST, + $connectSrc, + sprintf( + 'Prod nginx CSP connect-src must whitelist %s (RFC-WS-7 §3.5). Found: %s', + self::PROD_GLITCHTIP_HOST, + $connectSrc, + ), + ); + } + } + + private function extractMetaCsp(string $html): ?string + { + if (preg_match('/ Crewli — Organizer - + + content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' http://localhost:8000 ws://localhost:5174 http://localhost:8200; form-action 'self'; base-uri 'self'"> diff --git a/deploy/nginx/csp-spa.conf b/deploy/nginx/csp-spa.conf index d4a89894..76c2f5ce 100644 --- a/deploy/nginx/csp-spa.conf +++ b/deploy/nginx/csp-spa.conf @@ -2,14 +2,18 @@ # Vite bundles all JS/CSS into same-origin files. # 'unsafe-inline' for style-src is required by Vuetify (inline styles for theming). # img-src https: allows organisation logos loaded from external URLs. -# connect-src must include the API domain for XHR/fetch calls. +# connect-src must include: +# - https://api.crewli.app (XHR/fetch to the API) +# - https://monitoring.hausdesign.nl (RFC-WS-7 §3.5: GlitchTip event ingest; +# without it the browser silently blocks +# every @sentry/vue POST) # # IMPORTANT: Start with Content-Security-Policy-Report-Only to catch # false positives. Switch to Content-Security-Policy after 1-2 weeks # of clean logs. # Report-only mode (start with this): -# add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://api.crewli.app; frame-ancestors 'none'; form-action 'self'; base-uri 'self'" always; +# add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://api.crewli.app https://monitoring.hausdesign.nl; frame-ancestors 'none'; form-action 'self'; base-uri 'self'" always; # Enforce mode (switch to this after testing): -add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://api.crewli.app; frame-ancestors 'none'; form-action 'self'; base-uri 'self'" always; +add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://api.crewli.app https://monitoring.hausdesign.nl; frame-ancestors 'none'; form-action 'self'; base-uri 'self'" always; diff --git a/dev-docs/SECURITY_AUDIT.md b/dev-docs/SECURITY_AUDIT.md index c57e3d34..84d8b370 100644 --- a/dev-docs/SECURITY_AUDIT.md +++ b/dev-docs/SECURITY_AUDIT.md @@ -610,6 +610,7 @@ Audit scope: all files under `api/` and `apps/` (app, portal). - **Description:** ~~Neither app set a CSP meta tag or header.~~ - **Risk:** Injected scripts have unrestricted access. - **Resolution:** API CSP enforced via `SecurityHeaders` middleware (`default-src 'none'; frame-ancestors 'none'`). SPA CSP configured via Nginx snippets (`deploy/nginx/csp-spa.conf`, `csp-portal.conf`). Dev CSP meta tags added to all `index.html` files for local testing. See `deploy/README.md` for rollout instructions. +- **WS-7 follow-up (mei 2026):** SPA `connect-src` whitelists the GlitchTip event-ingest endpoint as an explicit security control — dev `http://localhost:8200`, prod `https://monitoring.hausdesign.nl` (RFC-WS-7 §3.5). This restricts outgoing observability traffic to a single known host; without it, the strict CSP would either silently drop events (PR-3 regression) or — if loosened blindly — allow exfiltration to arbitrary hosts. Regression-guard: `tests/Feature/Security/CspConnectsToObservabilityTest.php` reads both the dev meta tag and the production nginx config and asserts the host is present. #### [LOW] A13-10: No hardcoded secrets found in frontend code (positive)