Replit embeds are usually simple right up until CSP gets involved.

You drop in an iframe, everything looks fine locally, then production blocks it with a vague browser error. Or worse, your CSP is broad enough that it works, but now you’ve punched a bigger hole in your policy than you meant to.

This guide is the version I wish I had handy the last few times I dealt with third-party embeds.

The two CSP questions that matter

When you embed Replit content, there are two separate CSP concerns:

  1. Can your page load Replit inside a frame?
    That’s controlled by your frame-src directive.

  2. Can other sites embed your page?
    That’s controlled by frame-ancestors.

People mix these up constantly.

If you’re hosting a page on example.com and want to embed a Replit app in an iframe, you care about frame-src.
If you want to stop anyone else from putting your page in an iframe, you care about frame-ancestors.

If you want a deeper directive-by-directive breakdown later, csp-guide.com is a good reference.


Minimal CSP for a Replit iframe

If your page already has a CSP and you just need to allow a Replit embed, this is the smallest useful change:

Content-Security-Policy: default-src 'self'; frame-src 'self' https://replit.com;

And the HTML:

<iframe
  src="https://replit.com/@yourname/your-repl?embed=true"
  width="100%"
  height="600"
  loading="lazy"
  style="border:0;"
></iframe>

That works when the embedded content is served from replit.com.

Safer practical version

In real projects, I usually write it a little more explicitly:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  frame-src 'self' https://replit.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'self';

Why this shape?

  • default-src 'self' keeps the baseline tight
  • frame-src https://replit.com allows the embed
  • object-src 'none' shuts off old plugin junk
  • base-uri 'self' prevents <base> tag abuse
  • frame-ancestors 'self' stops random sites from embedding your page

If your site doesn’t need to be framed even by itself, use:

frame-ancestors 'none';

That’s what a lot of security-conscious apps do.


Nginx example

Copy-paste version for Nginx:

add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-src 'self' https://replit.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;

If you want to block all framing of your page:

add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-src 'self' https://replit.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;

Apache example

Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-src 'self' https://replit.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'"

Express / Node example

app.use((req, res, next) => {
  res.setHeader(
    "Content-Security-Policy",
    "default-src 'self'; " +
      "script-src 'self'; " +
      "style-src 'self' 'unsafe-inline'; " +
      "img-src 'self' data: https:; " +
      "frame-src 'self' https://replit.com; " +
      "object-src 'none'; " +
      "base-uri 'self'; " +
      "form-action 'self'; " +
      "frame-ancestors 'self'"
  );
  next();
});

With Helmet:

import helmet from "helmet";

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:"],
        frameSrc: ["'self'", "https://replit.com"],
        objectSrc: ["'none'"],
        baseUri: ["'self'"],
        formAction: ["'self'"],
        frameAncestors: ["'self'"],
      },
    },
  })
);

When frame-src is not enough

Sometimes a Replit embed pulls in assets or connections from other origins depending on the embed type, app runtime, or surrounding integration.

If your iframe itself loads but the embedded experience is broken, remember this:

Your page’s CSP does not control what happens inside Replit’s document.
Replit’s own CSP controls that.

Your CSP only decides whether the browser may load the iframe from that origin.

So if the browser error says something like:

Refused to frame ‘https://replit.com/' because it violates the following Content Security Policy directive: “frame-src ‘self’”

that’s your policy.

If the iframe appears but some internal feature inside it fails, that’s usually not your CSP.


Replit embed + existing production CSP

Most teams don’t start from a blank file. They start from a production CSP that already includes analytics, consent tools, fonts, and a pile of historical exceptions.

Here’s a real-world style header shape based on the CSP served by headertest.com:

content-security-policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-MTRiNjUyMmYtYWE3MS00Yjc5LWJmZTAtMDQwMDk0Yzg4MjE1' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
  frame-src 'self' https://consentcdn.cookiebot.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none'

If I wanted to add a Replit embed to that site, I would change only frame-src:

content-security-policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-MTRiNjUyMmYtYWE3MS00Yjc5LWJmZTAtMDQwMDk0Yzg4MjE1' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
  frame-src 'self' https://consentcdn.cookiebot.com https://replit.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none'

That’s the right habit: change the smallest directive that solves the problem.

If you want to sanity-check what your live headers actually look like, HeaderTest is handy for that.


Common mistakes

1. Using child-src and assuming it’s enough

Older CSP setups use child-src for frames. Modern policies should prefer frame-src.

If you have legacy browser support concerns, you can include both:

Content-Security-Policy: default-src 'self'; child-src https://replit.com; frame-src https://replit.com;

2. Adding Replit to script-src

You usually do not need that just to embed an iframe.

This is wrong for a plain iframe embed:

script-src 'self' https://replit.com;

It doesn’t permit framing. frame-src does.

3. Forgetting frame-ancestors

A lot of sites lock down outgoing resources carefully and then leave themselves embeddable by anyone.

If clickjacking matters, set one of these:

frame-ancestors 'none';

or

frame-ancestors 'self';

4. Going too broad with https:

This technically works:

frame-src https:;

I hate this for production. It allows framing from any HTTPS origin. If you only need Replit, allow Replit.


Report-only first if you’re nervous

If your CSP is already doing a lot, deploy the change in report-only mode first:

Content-Security-Policy-Report-Only: default-src 'self'; frame-src 'self' https://replit.com; report-to csp-endpoint;

Or with report-uri if you still use it:

Content-Security-Policy-Report-Only: default-src 'self'; frame-src 'self' https://replit.com; report-uri /csp-report;

That lets you see violations before enforcing the policy.


Quick recipes

Allow Replit embeds and block others from framing your page

Content-Security-Policy: default-src 'self'; frame-src 'self' https://replit.com; frame-ancestors 'none'; object-src 'none'; base-uri 'self';

Allow Replit embeds on a marketing page with inline styles

Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-src https://replit.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self';

Add Replit to an existing CSP

Before:

frame-src 'self' https://consentcdn.cookiebot.com;

After:

frame-src 'self' https://consentcdn.cookiebot.com https://replit.com;

Browser error cheat sheet

Error: refused to frame because of frame-src

Your site’s CSP is blocking the embed.

Fix:

frame-src https://replit.com;

Error: refused to frame because of frame-ancestors

Usually the embedded site is saying it does not want to be framed by your page.

That is not something you can override with your own CSP.

The iframe loads blank with no CSP error

Check:

  • X-Frame-Options from the embedded origin
  • frame-ancestors on the embedded origin
  • mixed content issues
  • sandbox attributes on your iframe
  • ad blockers or privacy extensions

My default recommendation

For most developer sites embedding a Replit project, I’d start here:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  frame-src 'self' https://replit.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

It’s boring, tight, and easy to reason about. That’s what you want from CSP.

If the embed needs more, expand one directive at a time and verify with browser devtools. Don’t “fix” a CSP issue by turning your policy into https: * 'unsafe-inline' 'unsafe-eval' chaos. I’ve seen that move too many times, and it always comes back to bite later.