If you embed Fillout on a site with a real Content Security Policy, you’ll usually hit one of two problems fast:

  1. the form iframe gets blocked
  2. the form loads, but some supporting requests fail silently

This is normal. CSP is doing its job.

The trick is knowing which directives matter for Fillout and which ones don’t. A lot of developers throw https: into half the policy and call it done. That works, but it defeats the point of having CSP in the first place.

Here’s how I’d approach CSP for Fillout in production.

How Fillout is usually embedded

Most teams use Fillout in one of these ways:

  • embedded as an iframe
  • opened in a popup or modal driven by a script
  • linked as a separate hosted form page
  • served from a custom domain

Each integration changes which CSP directives you need.

A basic iframe embed looks like this:

<iframe
  src="https://form.fillout.com/t/abcd1234"
  width="100%"
  height="600"
  frameborder="0"
  title="Contact form">
</iframe>

If your CSP blocks frames from Fillout, this won’t render.

The minimum CSP you usually need

For a plain iframe embed, the key directive is frame-src.

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

That allows your page to embed Fillout from form.fillout.com.

If you also load any Fillout JavaScript SDK or embed helper, you’ll need script-src too. Don’t add that unless you actually use it.

Which CSP directives matter for Fillout

Here’s the practical breakdown.

frame-src

This controls whether your page can embed Fillout in an iframe.

frame-src 'self' https://form.fillout.com;

If you use a Fillout custom domain, use that exact domain instead:

frame-src 'self' https://forms.example.com;

script-src

Only needed if you load Fillout JavaScript directly on your page.

For example:

<script src="https://server.fillout.com/embed/v1/"></script>

Then your CSP needs to allow that script origin:

script-src 'self' https://server.fillout.com;

If your site already uses nonces, keep using them. I’d strongly prefer a nonce-based policy over sprinkling hostnames everywhere.

Example:

script-src 'self' 'nonce-rAnd0m123' https://server.fillout.com;

And in HTML:

<script nonce="rAnd0m123" src="https://server.fillout.com/embed/v1/"></script>

connect-src

This one is easy to miss.

If your own page-side JavaScript talks to Fillout APIs, or Fillout’s embed script makes XHR/fetch/WebSocket calls from your page context, those requests are governed by connect-src.

A conservative example:

connect-src 'self' https://*.fillout.com;

I’d start narrow if you know the exact hostnames. If you don’t, use report-only mode first and inspect violations.

img-src, style-src, font-src

For a simple iframe embed, the iframe’s internal resources are governed by the iframe page’s CSP, not yours. That means your top-level CSP usually does not need to allow Fillout images, fonts, or styles just because the iframe uses them.

You only need to touch these directives if you load Fillout assets directly into your own document.

That distinction saves a lot of pointless policy bloat.

A realistic starting policy

If your site embeds Fillout in an iframe and nothing else, I’d start here:

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

That’s clean and strict.

If you use Fillout’s embed script

Some teams prefer a script-powered embed instead of writing the iframe manually.

Example HTML:

<div data-fillout-id="abcd1234" data-fillout-embed-type="standard"></div>
<script src="https://server.fillout.com/embed/v1/"></script>

Then your policy probably needs at least:

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

Why the 'unsafe-inline' in style-src? Because a lot of embed widgets inject inline styles. I don’t love it, but sometimes it’s the practical answer. If you can verify Fillout’s embed doesn’t need inline styles in your setup, remove it.

Custom domains change everything

If your Fillout form is served from a custom domain like forms.example.com, stop allowing generic Fillout hosts in frame-src unless you still need them.

Use your exact host:

frame-src 'self' https://forms.example.com;

If scripts are also served from that custom domain:

script-src 'self' https://forms.example.com;
connect-src 'self' https://forms.example.com;

This is one of the easiest wins in CSP: prefer exact origins over broad wildcards.

Learning from a real production CSP

Here’s a real CSP header from headertest.com:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-MDVmZGM2ZWEtNzczNi00ODU2LTliODUtMWYwNjY3MTY5YTk1' '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'

A few things I like here:

  • object-src 'none'
  • base-uri 'self'
  • nonce-based script-src
  • frame-ancestors 'none' for clickjacking protection

A few things I’d think about before copying this pattern for Fillout:

  • default-src includes third-party origins, which can blur policy intent
  • img-src https: is broad
  • style-src 'unsafe-inline' is a compromise, not a goal

For Fillout, I’d keep the same disciplined structure but add only the exact Fillout origins I need.

For example:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{ .CSPNonce }}' https://server.fillout.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://*.fillout.com;
  frame-src 'self' https://form.fillout.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';

That’s much easier to reason about than dumping Fillout into default-src.

Express example

If you’re setting CSP in Node/Express with Helmet:

import express from "express";
import helmet from "helmet";
import crypto from "crypto";

const app = express();

app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString("base64");
  next();
});

app.use((req, res, next) => {
  helmet({
    contentSecurityPolicy: {
      useDefaults: false,
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: [
          "'self'",
          (req, res) => `'nonce-${res.locals.cspNonce}'`,
          "https://server.fillout.com",
        ],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:"],
        fontSrc: ["'self'"],
        connectSrc: ["'self'", "https://*.fillout.com"],
        frameSrc: ["'self'", "https://form.fillout.com"],
        baseUri: ["'self'"],
        formAction: ["'self'"],
        objectSrc: ["'none'"],
        frameAncestors: ["'none'"],
      },
    },
  })(req, res, next);
});

app.get("/", (req, res) => {
  res.send(`
    <!doctype html>
    <html>
      <body>
        <div data-fillout-id="abcd1234" data-fillout-embed-type="standard"></div>
        <script nonce="${res.locals.cspNonce}" src="https://server.fillout.com/embed/v1/"></script>
      </body>
    </html>
  `);
});

app.listen(3000);

Nginx example

If you just need an iframe embed:

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

Debugging blocked Fillout embeds

The browser console will usually tell you exactly which directive is failing.

Common messages look like:

  • refused to frame because it violates frame-src
  • refused to load the script because it violates script-src
  • refused to connect because it violates connect-src

My workflow is simple:

  1. start with Content-Security-Policy-Report-Only
  2. load the page and interact with the form
  3. collect violations
  4. add the minimum required origins
  5. switch to enforcing mode

Example report-only header:

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

If you want ready-to-use policy patterns, csp-examples.com is useful for comparing approaches before you tighten them for your own stack.

A few hard-earned rules

I stick to these when embedding third-party forms:

  • never rely on default-src for third-party embeds
  • prefer frame-src over broad allowlists
  • keep Fillout origins out of directives that don’t need them
  • use exact hosts when you have a custom domain
  • treat 'unsafe-inline' as a temporary compromise
  • use report-only mode before shipping policy changes

If all you need is a Fillout iframe, your CSP can stay pretty tight. That’s the nice part about iframe-based integrations: most of the complexity stays isolated inside the frame.

If you start loading Fillout scripts into your own page, that’s when your policy gets wider and the tradeoffs get real.