Skip to content

fix(security): allow same-origin iframes via CSP frame-src 'self' (#1579)#1580

Merged
steilerDev merged 1 commit into
betafrom
fix/1579-auto-itemize-pdf-csp
May 25, 2026
Merged

fix(security): allow same-origin iframes via CSP frame-src 'self' (#1579)#1580
steilerDev merged 1 commit into
betafrom
fix/1579-auto-itemize-pdf-csp

Conversation

@steilerDev
Copy link
Copy Markdown
Owner

Summary

  • The auto-itemize PDF iframe was blocked by Helmet's CSP frame-src defaulting to 'none'; PDF would silently fail to render in the viewer
  • Added frameSrc: ["'self'"] to helmetPlugin.ts to permit same-origin (and blob:) iframes without loosening cross-origin embedding restrictions
  • X-Frame-Options: DENY (via Helmet's frameguard) is preserved unchanged — Cornerstone itself cannot be framed by third-party sites

Fixes #1579

Test plan

  • Unit test: helmetPlugin.test.ts asserts Content-Security-Policy header includes frame-src 'self'
  • E2E tests: invoice-auto-itemize-page.spec.ts covers page load, PDF iframe visibility after upload, and extracted line items — runs across desktop, tablet, and mobile viewports
  • Quality Gates pass (CI)

Docker preview image

A preview image will be published to DockerHub once CI passes:
steilerdev/cornerstone:pr-<PR_NUMBER>

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) noreply@anthropic.com

)

The auto-itemize PDF viewer embeds the uploaded PDF in an <iframe> so users
can review the document alongside the extracted line items. When Helmet's
Content Security Policy was applied, the `frame-src` directive defaulted to
`'none'`, which caused the browser to block the blob: URL used as the iframe
src and silently prevented the PDF from rendering.

## Changes

- **helmetPlugin.ts**: Add `frameSrc: ["'self'"]` to the `contentSecurityPolicy`
  directives object. The `frame-src` directive controls which URLs may be loaded
  as nested browsing contexts (iframes, frames). Setting it to `'self'` permits
  same-origin iframes (including blob: URLs created by the same origin) while
  still blocking any cross-origin iframe embedding attempts.

  Note: `frame-src` (what *this* page may embed) is distinct from the
  `X-Frame-Options: DENY` header (whether *other* pages may embed this app).
  Both directives are applied — Helmet's `frameguard` keeps `X-Frame-Options: DENY`
  unchanged, preventing Cornerstone itself from being framed by third-party sites.

## Tests

- **helmetPlugin.test.ts**: New assertion verifies that the CSP `Content-Security-Policy`
  response header includes `frame-src 'self'`, confirming the directive is emitted
  correctly in all responses.

- **invoice-auto-itemize-page.spec.ts**: New E2E test coverage for the auto-itemize
  PDF page, including smoke test that the page loads, the PDF iframe is visible after
  file upload, and extracted line items are rendered. Tests run across desktop, tablet,
  and mobile viewports.

Fixes #1579

Co-Authored-By: Claude backend-developer (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
@steilerDev
Copy link
Copy Markdown
Owner Author

[security-engineer]

APPROVED.

Security Review — CSP frame-src Loosening

Reviewed all six items in the verification checklist.

1. X-Frame-Options: SAMEORIGIN — unchanged and sound

frameguard: { action: 'sameorigin' } is present on line 26 of helmetPlugin.ts and is untouched. This controls whether our app can be embedded by a third-party origin. The new frame-src 'self' controls what we can embed. The two directives operate on orthogonal threat models and are correctly paired: third parties cannot iframe Cornerstone; Cornerstone can iframe its own same-origin endpoints. No regression.

2. object-src: "'none'" — unchanged

objectSrc: ["'none'"] is still present on line 19. <object> and <embed> rendering remains fully blocked. No regression.

3. All other CSP directives — untouched

defaultSrc, scriptSrc, styleSrc, imgSrc, fontSrc, connectSrc, baseUri, and formAction are all unchanged 'self'-only (or narrow explicit allowlists). Confirmed by direct file read.

4. No new third-party origins

The only change is 'none''self'. No external hostname was added.

5. Clickjacking / data exfiltration / content injection risk

No new risk introduced. The iframe src is constructed from a TypeScript-typed number parameter in getDocumentPreviewUrl(parseInt(documentId, 10)), which resolves to the fixed path /api/paperless/documents/:id/preview. The route schema enforces a numeric id param; auth is required on that endpoint; and the Content-Type returned is sanitized server-side via sanitizeBinaryContentType(). There is no user-supplied URL, no open redirect, and no path for a cross-origin document to be injected into the iframe.

The loosened policy allows this app to embed any same-origin response in an iframe — but the only actual callers are the one known PDF preview use. An attacker who could inject an arbitrary same-origin path into an iframe src would need a separate DOM injection vulnerability first, which the rest of the CSP (scriptSrc 'self', no unsafe-inline on script) already mitigates.

6. CSP enforcement mode — no report-only

Not in use in this codebase. The policy is enforced immediately. Acceptable for a self-hosted, operator-controlled deployment.

7. Other iframe usage in the codebase

Searched all .tsx and .ts files under client/src/. The only production <iframe> JSX is in AutoItemizePage.tsx (lines 1030–1036). The only other iframe-related file is AutoItemizePage.test.tsx. No other component inadvertently benefits from this policy change.

Summary

The change is minimal and correctly scoped. The two-layer clickjacking defence (X-Frame-Options: SAMEORIGIN + frameguard) is intact. The unit test added to helmetPlugin.test.ts correctly asserts both the new frame-src 'self' and the unchanged object-src 'none'. The E2E scenario 18 test adds browser-level coverage. No security concerns — merge can proceed.

@steilerDev steilerDev merged commit 8819e37 into beta May 25, 2026
30 of 32 checks passed
@steilerDev steilerDev deleted the fix/1579-auto-itemize-pdf-csp branch May 25, 2026 19:57
@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.7.0-beta.15 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant