UN-2238 [FIX] Enforce the frontend CSP (out of report-only mode) - #2245
Open
Deepak-Kesavan wants to merge 3 commits into
Open
Deepak-Kesavan wants to merge 3 commits into
Deepak-Kesavan wants to merge 3 commits into
Conversation
Sweep of the report-only policy on a live deployment (browsing + per-directive probes) turned up hosts the shipped bundle loads but no directive allows: - style-src/font-src: cdn.jsdelivr.net (Monaco CSS + codicon) - img-src: cdn.jsdelivr.net (emoji-datasource), ProductFruits, GTM, GA, reCAPTCHA assets, q.stripe.com - media-src: cdn.productfruits.com (new directive; default-src had no blob:) - connect-src: unpkg.com, api.productfruits.com, GA4 regional endpoints, reCAPTCHA api2, m.stripe.network - frame-src: googletagmanager ns.html, m.stripe.network ProductFruits' animations.css was the one violation observed in normal use; the rest belong to code paths and flows that were not exercised. Drops the bare `wss:` wildcard: socket.io connects to window.location.origin (GetStaticData getBaseUrl) and 'self' covers same-origin ws/wss per CSP3, verified with a ws:// probe against nginx serving this policy. Adds .claude/skills/csp-check so the policy can be re-checked against a build or a deployment when a frontend dependency changes.
Contributor
|
Renames the header from Content-Security-Policy-Report-Only to Content-Security-Policy. The policy value is byte-identical to the one the previous commit landed after the origin sweep -- the only change is that a resource from an unlisted origin is now blocked rather than logged. That sweep predated the shadcn/React 19 frontend migration, so it was rechecked against both bundles before flipping: - the OSS build (`bun run build`, 111 chunks): every external host it references is in the policy - the live us-central deployment (363 chunks, which includes the cloud plugin code the OSS build does not have): same The one host either scan turned up that no directive allows is react.dev, which appears only in React 19's error-message links and is never fetched -- added to scan_origins.py's IGNORED set rather than to the policy. Also refreshes the csp-check skill: it described a report-only policy and a `npm run build`/frontend/dist layout that is now `bun run build`/frontend/build.
Contributor
Frontend Lint Report (Biome)✅ All checks passed! No linting or formatting issues found. |
|
Contributor
Unstract test resultsPer-group results
Critical paths
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What
Takes the frontend CSP out of report-only mode, which is what UN-2238 asks for. Two commits:
wss:wildcard fromconnect-src. Adds.claude/skills/csp-check— scripts + procedure for re-checking the policy against a build or a running deployment (the "way to check compatibility of new features with the CSP policy" Hari asked for in the ticket).Content-Security-Policy-Report-OnlytoContent-Security-Policy. The policy value is byte-identical across that commit — the only change is that an unlisted origin is now blocked rather than logged.Why
UN-2238. CSP shipped in report-only mode with no report collector, so violations only ever reached each user's browser console. The ticket's exit condition is "once there are no more violations, we can take CSP out of report mode" — commit 1 closes the violations, commit 2 does the flip.
How
Commit 1 — what the sweep added
Two methods: browsing while recording
securitypolicyviolation(landing, dashboard, prompt studio incl. PDF viewer, API/ETL/task, logs, users, LLM settings, platform settings, profile, manual review), plus per-directive probes for hosts the bundle references whose code paths were not exercised. Each host below traces to a chunk in the deployed bundle.style-srccdn.jsdelivr.net,app.productfruits.comeditor.main.css; ProductFruits CSS (observed in normal use)font-srccdn.jsdelivr.netcodicon.ttfimg-srccdn.jsdelivr.net,app.productfruits.com,cdn.productfruits.com,www.googletagmanager.com,www.google-analytics.com,www.google.com/recaptcha/,www.gstatic.com/recaptcha/,q.stripe.comconnect-srcunpkg.com,api.productfruits.com,www.googletagmanager.com,region1.google-analytics.com,analytics.google.com,www.google.com/recaptcha/,m.stripe.networkframe-srcwww.googletagmanager.com,m.stripe.networkns.html; Stripe.jsmedia-src'self' blob: cdn.productfruits.comdefault-src 'self', noblob:)Commit 2 — rechecking before the flip
That sweep predates the shadcn / React 19 frontend migration (359 files), so the policy was rechecked against both bundles:
bun run build, 111 chunks) — every external host it references is in the policy.The one host either scan turned up that no directive allows is
react.dev, which appears only in React 19's error-message links and is never fetched. It went intoscan_origins.py'sIGNOREDset, not into the policy.The skill's docs were also refreshed: they described a report-only policy and an
npm run build/frontend/distlayout that is nowbun run build/frontend/build.Can this PR break any existing features. If yes, please list possible items. If no, please explain why.
Yes — this is the commit where a policy gap stops being a console message and starts being a broken feature. What was checked, and what is left:
animations.css), which commit 1 allows.window.location.href = ${origin}/api/v1/login— a navigation, whichform-actiondoes not govern. App forms are ReactonSubmithandlers with no native submit./api/v1/...paths and socket.io connects towindow.location.origin;'self'covers both, including same-origin ws/wss under CSP3.frame-ancestors 'self'was already enforced in practice by the existingX-Frame-Options: SAMEORIGIN.VITE_CUSTOM_LOGO_URLandVITE_FAVICON_PATH(viagenerate-runtime-config.sh) let a deployment point the logo/favicon at an arbitrary URL. Underimg-src, an external logo host now has to be added to the policy or the logo will not render. Same-origin values are unaffected..claude/skills/csp-checkcheck 3 is the procedure.Rollback is a one-line revert of commit 2.
Database Migrations
Env Config
VITE_CUSTOM_LOGO_URL/VITE_FAVICON_PATHnote above for an existing var whose behaviour now has a CSP constraint.Relevant Docs
.claude/skills/csp-check/SKILL.mdRelated Issues or PRs
Dependencies Versions
Notes on Testing
scan_origins.py --dist frontend/buildagainst a local production build of this branch: clean, 111 chunks.scan_origins.py --url https://us-central.unstract.com: clean, 363 chunks.extract_policy.pyparses the flipped header and reports 13 directives, and the policy string is byte-identical to the pre-flip one (verified by diffing the twoadd_headervalues).docker runofnginx:alpinewith thisnginx.conf— config accepted, header emitted verbatim; 48 per-directive browser probes against it — every listed origin loads with no report, and all 4 controls to a disallowed host were reported. The conf's directive structure is unchanged since, only the header name differs. Docker was not available in this round, so that check was not re-run.