Iframe Embedding Security#
Dify uses a defense-in-depth approach to prevent clickjacking and XSS via embedded iframes. Controls are applied at two enforcement points: the Next.js middleware layer (frontend) and the Flask API layer (backend file preview endpoints).
Frontend: Next.js Middleware (web/proxy.ts)#
The primary enforcement point is web/proxy.ts, which runs as Next.js middleware on every matched request.
Frame protection logic (lines 51β63): The wrapResponseWithFrameProtection() function sets both X-Frame-Options: DENY and Content-Security-Policy: frame-ancestors 'none' unless the request path is explicitly embeddable.
A path is embeddable if all of the following are true:
NEXT_PUBLIC_ALLOW_EMBED=true(operator opt-in, defaultsfalse)- The path is not in
NON_EMBEDDABLE_PATH_SEGMENTS(currently:/device) - The path is in
EMBEDDABLE_PATH_SEGMENTS(lines 13β20):/agent,/chat,/chatbot,/completion,/webapp-signin,/workflow
These embeddable paths correspond to published app routes (chatbot widgets, workflow UIs, etc.) intended for third-party embedding. All other paths β including the console UI β are blocked from framing by default.
Path matching uses matchesPathSegment(), which requires an exact segment match or a segment/ prefix. This prevents lookalike paths (e.g., /chatty, /workflowish) from bypassing the allow-list.
CSP with nonce (lines 83β135): When NEXT_PUBLIC_CSP_WHITELIST is set and NODE_ENV=production, the middleware generates a per-request nonce and sets a full Content-Security-Policy header. wrapResponseWithFrameProtection() is always called last, appending frame-ancestors 'none' to the CSP for protected routes regardless of whether the whitelist CSP is active.
NEXT_PUBLIC_ALLOW_EMBED (line 22): Declared in web/env.ts with an inline comment citing clickjacking as the rationale. Defaults to false. When true, published app routes become embeddable, but /device routes remain protected regardless.
Backend: File Preview Endpoints#
File preview endpoints apply a broader set of security headers to prevent XSS via user-uploaded files (added in PR #28910):
Content-Security-Policy: default-src 'none'; sandboxX-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: no-referrer
Additionally, enforce_download_for_html() detects HTML content by MIME type (text/html, application/xhtml+xml) or file extension (.html, .htm) and forcibly overwrites the response with Content-Disposition: attachment, Content-Type: application/octet-stream, and X-Content-Type-Options: nosniff. This prevents an attacker from uploading an HTML file and triggering same-origin script execution when it's previewed in a browser.
Known Issue: Console Auth Leakage into Share Pages (v1.15.0)#
A regression in v1.15.0 caused the useTimestamp() hook to fire GET /console/api/account/profile from public webapp routes (/chat/{token}, etc.). Without a console session, this triggered a 401 β redirect to /signin chain, making public web apps inaccessible to anonymous users β and breaking any embedded iframe that relied on public access .
Root cause: useTimestamp() was called inside useChat() on share-layout pages that are explicitly designed to operate without console authentication. Fixed in PR #37915.
Configuration Reference#
| Variable | Default | Effect |
|---|---|---|
NEXT_PUBLIC_ALLOW_EMBED | false | Set true to allow iframe embedding of published app routes; /device remains non-embeddable |
NEXT_PUBLIC_CSP_WHITELIST | (unset) | Enables full nonce-based CSP in production; space-separated domain list |
See docker/.env.example for the canonical list of configurable variables.
Key Source Files#
| File | Role |
|---|---|
web/proxy.ts | Next.js middleware; wrapResponseWithFrameProtection(), EMBEDDABLE_PATH_SEGMENTS, nonce CSP |
web/env.ts | NEXT_PUBLIC_ALLOW_EMBED and NEXT_PUBLIC_CSP_WHITELIST env schema and defaults |
api/controllers/common/file_response.py | enforce_download_for_html() helper |
api/controllers/files/image_preview.py | File preview API; applies security headers |