Resolve ALLOWED_FRAME_URLS from the runtime environment in Docker#4356
Resolve ALLOWED_FRAME_URLS from the runtime environment in Docker#4356Niki2k1 wants to merge 1 commit into
Conversation
|
@Niki2k1 is attempting to deploy a commit to the Umami Software Team on Vercel. A member of the Team first needs to authorize it. |
af6a268 to
2d6ad04
Compare
Greptile SummaryThis PR fixes a longstanding issue where
Confidence Score: 4/5Safe to merge; the runtime CSP path works correctly in Docker and the non-Docker build path is unchanged. The core logic is sound: CSP is correctly constructed from runtime env vars, the middleware stamps it on every response, and the dual-header problem is properly avoided by suppressing the build-time header. The two findings are minor — a per-request recomputation that could be lifted to module scope, and a filesystem sentinel that could break silently if the Dockerfile is restructured. The existsSync('./src/proxy.ts') check in next.config.ts warrants a second look: it is the load-bearing mechanism that prevents two CSP headers from being emitted, but it relies on an implicit Dockerfile convention rather than an explicit signal. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
subgraph Docker Build
A[next build runs] --> B{existsSync src/proxy.ts?}
B -- YES Docker context --> C[Skip CSP in routes-manifest.json]
B -- NO Local dev context --> D[Bake CSP into routes-manifest.json]
end
subgraph Docker Runtime - Per Request
E[Incoming request] --> F[proxy.ts middleware matcher /:path*]
F --> G{Matches custom collect/script/login?}
G -- YES --> H[Return rewrite/redirect]
G -- NO --> I[NextResponse.next]
H --> J[res.headers.set Content-Security-Policy reads ALLOWED_FRAME_URLS at runtime]
I --> J
J --> K[Response to browser Single CSP header]
end
subgraph Non-Docker Runtime
L[Incoming request] --> M[Next.js serves baked CSP header from routes-manifest.json ALLOWED_FRAME_URLS frozen at build time]
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
subgraph Docker Build
A[next build runs] --> B{existsSync src/proxy.ts?}
B -- YES Docker context --> C[Skip CSP in routes-manifest.json]
B -- NO Local dev context --> D[Bake CSP into routes-manifest.json]
end
subgraph Docker Runtime - Per Request
E[Incoming request] --> F[proxy.ts middleware matcher /:path*]
F --> G{Matches custom collect/script/login?}
G -- YES --> H[Return rewrite/redirect]
G -- NO --> I[NextResponse.next]
H --> J[res.headers.set Content-Security-Policy reads ALLOWED_FRAME_URLS at runtime]
I --> J
J --> K[Response to browser Single CSP header]
end
subgraph Non-Docker Runtime
L[Incoming request] --> M[Next.js serves baked CSP header from routes-manifest.json ALLOWED_FRAME_URLS frozen at build time]
end
Reviews (1): Last reviewed commit: "Resolve ALLOWED_FRAME_URLS from the runt..." | Re-trigger Greptile |
| // Set the CSP here, not only at build time in next.config.ts, so | ||
| // ALLOWED_FRAME_URLS is resolved from the runtime environment. | ||
| res.headers.set('Content-Security-Policy', getContentSecurityPolicy()); |
There was a problem hiding this comment.
getContentSecurityPolicy() reads process.env.ALLOWED_FRAME_URLS and process.env.API_URL and rebuilds the CSP string on every single request. Because environment variables are immutable once the Node process has started, the return value never changes between calls. Computing it once at module initialization (or lazily on the first call) avoids redundant string allocations on every request through this middleware.
| // Set the CSP here, not only at build time in next.config.ts, so | |
| // ALLOWED_FRAME_URLS is resolved from the runtime environment. | |
| res.headers.set('Content-Security-Policy', getContentSecurityPolicy()); | |
| // Set the CSP here, not only at build time in next.config.ts, so | |
| // ALLOWED_FRAME_URLS is resolved from the runtime environment. | |
| // Computed once at module load — env vars don't change after process start. | |
| res.headers.set('Content-Security-Policy', CSP); |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| // The Docker proxy (src/proxy.ts) sets the CSP per request so ALLOWED_FRAME_URLS | ||
| // works at runtime; skip the build-time header there to avoid sending two CSPs | ||
| // (browsers intersect duplicates, which would drop the runtime frame-ancestors). | ||
| const runtimeCsp = existsSync('./src/proxy.ts'); |
There was a problem hiding this comment.
Implicit Docker detection via filesystem sentinel
existsSync('./src/proxy.ts') works because the Dockerfile copies docker/proxy.ts into src/ before running next build. However, this implicitly couples next.config.ts to the Dockerfile's COPY step — if the Dockerfile is restructured, the copy step is reordered, or next build is run from a non-root cwd, the check silently returns false and the build-time CSP header is re-emitted alongside the runtime one (causing browsers to intersect both and silently drop frame-ancestors). An explicit build-arg-turned-env-var (e.g. ARG RUNTIME_CSP → ENV RUNTIME_CSP=1 in the Dockerfile, checked here as process.env.RUNTIME_CSP) would make the intent explicit and survive Dockerfile refactors.
2d6ad04 to
29c5427
Compare
The Content-Security-Policy is defined in next.config.ts headers(), which Next serializes at build time, so ALLOWED_FRAME_URLS only takes effect when it is set before `next build`. The published Docker images are built without it, so frame-ancestors cannot be configured at runtime and iframe embedding requires a custom image (umami-software#3793, umami-software#2407). Move the policy into a shared getContentSecurityPolicy() and set the header from the Docker proxy (docker/proxy.ts) on every response, where runtime env such as CORS_MAX_AGE and DISABLE_LOGIN is already handled. next.config.ts skips the build-time CSP when the proxy is present, so exactly one header is sent. Non-Docker builds are unchanged: the build-time CSP still applies and ALLOWED_FRAME_URLS keeps working as a build-time variable there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
29c5427 to
ecf9b3b
Compare
Problem
ALLOWED_FRAME_URLSis documented as the way to allow iframe embedding, but it is effectively a build-time variable: the CSP is built innext.config.tsheaders(), which Next serializes intoroutes-manifest.jsonduringnext build. The published Docker images are built without it, soframe-ancestorsis fixed at'self'and setting the env on the running container has no effect — embedding currently requires building a custom image.Reported in #3793 and #2407.
Change
getContentSecurityPolicy()(src/lib/csp.ts) that reads the environment when called.docker/proxy.ts) on every response, alongside the other runtime env it already handles (CORS_MAX_AGE,DISABLE_LOGIN, …). This makesALLOWED_FRAME_URLS(andAPI_URLinconnect-src) resolve at runtime.next.config.tsskips the build-time CSP when the proxy is present (src/proxy.ts, which the Dockerfile copies in), so browsers receive exactly one CSP header — two would be intersected, dropping the runtimeframe-ancestors.Non-Docker builds are unchanged:
next.config.tsstill emits the build-time CSP, andALLOWED_FRAME_URLSkeeps working as a build-time variable there.Verification
Built the image once, then started the same image with different runtime values:
ALLOWED_FRAME_URLS(runtime env)frame-ancestorsin responsehttps://example.com'self' https://example.comhttps://example.com https://foo.example'self' https://example.com https://foo.example'self'Exactly one
Content-Security-Policyheader is returned in each case.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.