All guides

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 directives into its default policy. You keep the other defaults and change only frameAncestors.
  • xFrameOptions: false stops the X-Frame-Options header. Older Helmet versions call this option frameguard. Helmet 8 still accepts frameguard as 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-FROM set by hand. Current Helmet throws an error for action: "allow-from". A hand-written X-Frame-Options: ALLOW-FROM header is ignored by browsers, so it does not limit framing. Use frameAncestors.
  • contentSecurityPolicy: false to fix framing. This removes the whole CSP, not only frame-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.

Sources

Check your page now

Know before you embed.

Free, no sign-up, nothing stored. Paste a URL and get a verdict, a live preview, and the fix.