Skip to content

UN-2238 [FIX] Enforce the frontend CSP (out of report-only mode) - #2245

Open
Deepak-Kesavan wants to merge 3 commits into
mainfrom
UN-2238-csp-policy
Open

Deepak-Kesavan wants to merge 3 commits into
mainfrom
UN-2238-csp-policy

Conversation

@Deepak-Kesavan

@Deepak-Kesavan Deepak-Kesavan commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

What

Takes the frontend CSP out of report-only mode, which is what UN-2238 asks for. Two commits:

  1. Widen the report-only policy to the third-party origins the shipped frontend actually loads, found by sweeping a live deployment, and drop the bare wss: wildcard from connect-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).
  2. Flip the header from Content-Security-Policy-Report-Only to Content-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.

Directive Added Loaded by
style-src cdn.jsdelivr.net, app.productfruits.com Monaco editor.main.css; ProductFruits CSS (observed in normal use)
font-src cdn.jsdelivr.net Monaco codicon.ttf
img-src cdn.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.com emoji-datasource; ProductFruits media; GTM/GA pixels; reCAPTCHA assets; Stripe beacons
connect-src unpkg.com, api.productfruits.com, www.googletagmanager.com, region1.google-analytics.com, analytics.google.com, www.google.com/recaptcha/, m.stripe.network LLMW pdf worker; ProductFruits API; GA4 endpoints; reCAPTCHA api2; Stripe.js
frame-src www.googletagmanager.com, m.stripe.network react-gtm ns.html; Stripe.js
media-src new directive: 'self' blob: cdn.productfruits.com ProductFruits video (previously fell back to default-src 'self', no blob:)

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:

  • OSS build (bun run build, 111 chunks) — every external host it references is in the policy.
  • 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. It went into scan_origins.py's IGNORED set, not into the policy.

The skill's docs were also refreshed: they described a report-only policy and an npm run build / frontend/dist layout that is now bun 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:

  • Everything the bundle references is covered by the two scans above, and the browse-through found exactly one real-use violation (ProductFruits animations.css), which commit 1 allows.
  • Login and form posts are unaffected. Login is window.location.href = ${origin}/api/v1/login — a navigation, which form-action does not govern. App forms are React onSubmit handlers with no native submit.
  • Same-origin API and sockets are unaffected. All API calls use relative /api/v1/... paths and socket.io connects to window.location.origin; 'self' covers both, including same-origin ws/wss under CSP3.
  • frame-ancestors 'self' was already enforced in practice by the existing X-Frame-Options: SAMEORIGIN.
  • Operator-facing change worth knowing: VITE_CUSTOM_LOGO_URL and VITE_FAVICON_PATH (via generate-runtime-config.sh) let a deployment point the logo/favicon at an arbitrary URL. Under img-src, an external logo host now has to be added to the policy or the logo will not render. Same-origin values are unaffected.
  • Not covered by a static scan: a host that is fetched only at runtime from a string the scanner cannot see, and a host allowed on the wrong directive (a font pulled from a script-src-only host still violates). The August browse-through covered this for the flows listed above; it has not been repeated against a deployment carrying the shadcn migration. That browse-through on a dev/QA deployment of this branch is the remaining pre-merge check.claude/skills/csp-check check 3 is the procedure.

Rollback is a one-line revert of commit 2.

Database Migrations

  • None.

Env Config

  • None added. See the VITE_CUSTOM_LOGO_URL / VITE_FAVICON_PATH note above for an existing var whose behaviour now has a CSP constraint.

Relevant Docs

  • .claude/skills/csp-check/SKILL.md

Related Issues or PRs

Dependencies Versions

  • None.

Notes on Testing

  • scan_origins.py --dist frontend/build against a local production build of this branch: clean, 111 chunks.
  • scan_origins.py --url https://us-central.unstract.com: clean, 363 chunks.
  • extract_policy.py parses the flipped header and reports 13 directives, and the policy string is byte-identical to the pre-flip one (verified by diffing the two add_header values).
  • From the previous round: docker run of nginx:alpine with this nginx.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.

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.
@greptile-apps

greptile-apps Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

via Greptile

RetriggerConfidence Score: 5/5

The PR appears safe to merge because no blocking failure remains within the eligible follow-up review scope.

Summary

The PR changes the frontend CSP from report-only to enforcement, expands directive-specific third-party allowlists, removes the unrestricted WebSocket source, and adds a manual CSP validation skill.

  • Adds required style, image, font, connection, frame, and media sources.
  • Adds scripts for extracting the policy, scanning bundle origins, and running browser probes.
  • Updates .gitignore so the new skill is tracked.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Frontend response] --> B[Enforcing CSP header]
  B --> C{Resource source allowed<br/>by applicable directive?}
  C -->|Yes| D[Browser loads resource]
  C -->|No| E[Browser blocks resource]
  F[CSP checking skill] --> G[Extract policy]
  F --> H[Scan bundle origins]
  F --> I[Run directive probes]
  G --> B
  H --> B
  I --> B
Loading

Reviews (2) · Last reviewed commit: "UN-2238 [FIX] Enforce the frontend CSP i..."

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.
@github-actions

Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

All checks passed! No linting or formatting issues found.

@sonarqubecloud

Copy link
Copy Markdown

@Deepak-Kesavan Deepak-Kesavan changed the title UN-2238 [FIX] CSP: allow third-party origins found in report-only sweep UN-2238 [FIX] Enforce the frontend CSP (out of report-only mode) Sep 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
frontend unit 0 1 0 0 0.0
unit-backend unit 1276 0 0 1 45.9
unit-connectors unit 63 0 0 0 10.2
unit-core unit 115 0 0 0 2.2
unit-platform-service unit 15 0 0 0 2.8
unit-rig unit 120 0 0 0 4.8
unit-runner unit 5 0 0 0 3.1
unit-sdk1 unit 563 0 0 0 30.1
unit-workers unit 1425 0 0 1 127.8
TOTAL 3582 1 0 2 227.0

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
💤 Covered, but not exercised in this build
  • auth-login — User can log in and obtain a session cookie. (covered by e2e-login; no result reported in this build)
  • adapter-register-llm — Register and validate an LLM adapter. (covered by integration-backend; no result reported in this build)
  • workflow-author — Create a workflow; its source+destination endpoints materialise and are configurable. (covered by integration-backend; no result reported in this build)
  • co-owner-manage — Add/remove co-owners of a shared resource; enforce the last-owner guard. (covered by integration-backend, e2e-coowners; no result reported in this build)
  • workflow-create-execute — Create a workflow, configure source+destination, execute, poll, fetch result. (covered by e2e-workflow; no result reported in this build)
  • api-deployment-provision — Deploying a workflow as an API mints a usable key and a resolvable endpoint. (covered by integration-backend; no result reported in this build)
  • api-deployment-auth — Unauthenticated or mis-scoped API-deployment calls are rejected before dispatch. (covered by integration-backend; no result reported in this build)
  • api-deployment-run — Deploy a workflow as an API, POST a document, receive structured JSON. (covered by e2e-api-deployment; no result reported in this build)
  • mcp-server-auth — Unauthenticated or mis-scoped hosted-MCP calls are rejected before any tool runs. (covered by integration-backend; no result reported in this build)
  • mcp-platform-auth — The org-scoped MCP endpoint stays behind the platform-API-key middleware; unauthenticated or mis-scoped calls reach no tool. (covered by integration-backend; no result reported in this build)
  • platform-key-whoami — A platform API key resolves its own organisation over the org-less whoami endpoint; the org comes from the key row, not the URL. (covered by integration-backend; no result reported in this build)
  • prompt-studio-author — Create a Prompt Studio project and add a prompt to it. (covered by integration-backend; no result reported in this build)
  • prompt-studio-fetch-response — Prompt Studio: create project, add prompt, run a prompt, get response. (covered by e2e-prompt-studio; no result reported in this build)
  • connector-register-test — Connector credentials are validated against the live system and stored encrypted. (covered by integration-backend; no result reported in this build)
  • pipeline-etl-execute — Run an ETL pipeline from source connector to destination. (covered by e2e-etl; no result reported in this build)
  • usage-aggregate-read — Per-run token usage aggregates correctly and stays scoped to its organization. (covered by integration-backend; no result reported in this build)
  • usage-token-tracking — Per-execution token usage is recorded and retrievable. (covered by e2e-api-deployment; no result reported in this build)
  • callback-result-delivery — Async results are posted back via the callback worker. (covered by e2e-api-deployment; no result reported in this build)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant