From 5c42f27b26150f33256847263312d30c5448c589 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Thu, 7 May 2026 18:36:05 +0200 Subject: [PATCH] fix: whitelist GlitchTip ingest host in CSP connect-src MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-3 follow-up. Live smoke surfaced that the @sentry/vue SDK was running correctly and emitting events, but Crewli's strict connect-src directive blocked every POST at the browser layer. No fallback — events evaporated silently with a CSP-violation log in DevTools console only. Updated locations (audited the CSP surface; only two locations actually need the whitelist): - apps/app/index.html — dev meta CSP, adds http://localhost:8200 to connect-src so local dev hits the docker-compose GlitchTip stack. - deploy/nginx/csp-spa.conf — prod organizer SPA CSP, adds https://monitoring.hausdesign.nl to BOTH the report-only and enforce add_header lines so a future flip between modes can't silently break observability. NOT updated (deviation from prompt): - api/config/security.php — the API CSP is `default-src 'none'; frame-ancestors 'none'` for JSON responses. Browsers don't enforce connect-src on JSON contexts (no document, no fetch origin). Adding connect-src would be semantically a no-op and confuse the deny-by- default policy. Regression guard: tests/Feature/Security/CspConnectsToObservabilityTest. Reads both the dev meta tag and the prod nginx conf directly (the SPA's CSP is not Laravel-served, so $this->get() can't reach it). Apply-with- revert verified: stashing both fixes makes both cases fail with a clear "Refused to connect because it violates the following CSP directive" hint; popping the stash restores green. SECURITY_AUDIT.md A13-9 updated with a WS-7 follow-up note documenting the GlitchTip whitelist as an explicit security control: outgoing observability traffic restricted to a single known host. Test count 1549 to 1551. Larastan + Pint clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../CspConnectsToObservabilityTest.php | 109 ++++++++++++++++++ apps/app/index.html | 7 +- deploy/nginx/csp-spa.conf | 10 +- dev-docs/SECURITY_AUDIT.md | 1 + 4 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 api/tests/Feature/Security/CspConnectsToObservabilityTest.php 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)