Allow embedding in Express
Helmet's defaults send X-Frame-Options SAMEORIGIN and a CSP with frame-ancestors 'self', so other sites cannot frame your app. Turn off xFrameOptions and set the frameAncestors directive to the sites you trust. Without Helmet, set the Content-Security-Policy header with res.setHeader.
4 min read · Updated
Why Express apps block frames
Express itself adds no framing header. Helmet does. With app.use(helmet()), every response gets:
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: default-src 'self'; ...; frame-ancestors 'self'; ...
Only pages from your own origin can frame the app.
Allow one partner site with Helmet
import express from "express";
import helmet from "helmet";
const app = express();
app.use(
helmet({
xFrameOptions: false,
contentSecurityPolicy: {
directives: {
frameAncestors: ["'self'", "https://partner.example"],
},
},
}),
);
- Helmet merges your
directivesinto its default policy. You keep the other defaults and change onlyframeAncestors. xFrameOptions: falsestops the X-Frame-Options header. Older Helmet versions call this optionframeguard. Helmet 8 still acceptsframeguardas an alias. Setting both throws an error, so set only one.- Keep the quotes inside the strings:
"'self'", not"self".
Allow framing on some routes only
Use the default Helmet for the app, and a second config for the embed routes. Mount the embed router first:
const embedHelmet = helmet({
xFrameOptions: false,
contentSecurityPolicy: {
directives: { frameAncestors: ["https://partner.example"] },
},
});
app.use("/embed", embedHelmet, embedRouter);
app.use(helmet());
app.use(mainRouter);
Requests to /embed get only the embed headers, because embedRouter sends the response before the main Helmet runs. If embedRouter calls next(), the main Helmet runs too and overwrites the headers.
Without Helmet
app.use("/embed", (req, res, next) => {
res.removeHeader("X-Frame-Options");
res.setHeader("Content-Security-Policy", "frame-ancestors 'self' https://partner.example");
next();
});
res.setHeader replaces a CSP set earlier in the same response. It cannot change a header that a later middleware sets. So put this after any code that adds security headers, or remove that code.
To allow a list of partners from config:
const partners = (process.env.FRAME_ANCESTORS ?? "").split(/\s+/).filter(Boolean);
const frameAncestors = ["'self'", ...partners].join(" ");
Check the result
curl -sI http://localhost:3000/embed/ | grep -iE "x-frame-options|content-security-policy"
You want one CSP line with your frame-ancestors and no X-Frame-Options. Then check the public URL. A proxy in front of Express can add headers again.
Common mistakes
- The proxy adds the header. Nginx or a CDN in front of Express can send its own X-Frame-Options or CSP. See Allow embedding on Nginx.
- Two CSP headers. Helmet sets one and the proxy adds another. The browser enforces both.
ALLOW-FROMset by hand. Current Helmet throws an error foraction: "allow-from". A hand-writtenX-Frame-Options: ALLOW-FROMheader is ignored by browsers, so it does not limit framing. UseframeAncestors.contentSecurityPolicy: falseto fix framing. This removes the whole CSP, not onlyframe-ancestors. Change the one directive.
Test the public URL
Paste the deployed URL into the iframe tester. It tells you if other sites can frame the page and which header to change if not.
To check many pages at once, use the bulk check. It shows X-Frame-Options and frame-ancestors for up to 100 URLs.