Paperform is easy to embed and easy to break with a bad CSP.
If you run a locked-down site and drop in a Paperform embed, you’ll usually hit one of these:
- the iframe refuses to load
- form assets get blocked
- custom scripts around the embed stop working
- submissions fail because
connect-srcis too strict - your own site can’t frame Paperform because
frame-srcis missing
This guide is the practical version: what to allow, when to allow it, and copy-paste policies you can start with.
The short version
If you embed a Paperform form in an iframe, your CSP usually needs at least:
Content-Security-Policy:
default-src 'self';
frame-src https://paperform.co https://*.paperform.co;
child-src https://paperform.co https://*.paperform.co;
script-src 'self';
style-src 'self';
img-src 'self' data: https:;
connect-src 'self';
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
object-src 'none';
That’s the baseline for your site embedding Paperform.
If you use Paperform on a custom domain, or load Paperform assets from another Paperform-controlled host, you’ll need to expand the policy. I’ll show that in a minute.
First: know what CSP controls here
When Paperform is embedded, there are two separate CSP concerns:
-
Your page’s CSP
- Controls whether your page is allowed to load the Paperform iframe.
- Controls any wrapper scripts, styles, analytics, and postMessage helpers on your page.
-
Paperform’s own CSP
- Controls what happens inside the Paperform document.
- You usually do not control this unless you fully proxy or host something custom.
That distinction matters. If your page blocks frame-src, the Paperform iframe never gets a chance to render.
Most common Paperform embedding pattern
A typical embed looks something like this:
<div data-paperform-id="my-form" data-height="600"></div>
<script src="https://paperform.co/__embed.min.js"></script>
Or an iframe embed:
<iframe
src="https://paperform.co/form/my-form"
width="100%"
height="600"
frameborder="0"
allowfullscreen>
</iframe>
These two patterns may need different directives:
script-srcfor the Paperform embed scriptframe-srcfor the iframeconnect-srcif your page-side code talks to APIsimg-srcandstyle-srcif the embed helper injects assets
CSP for iframe-based Paperform embeds
If you only use a plain iframe, start here:
Content-Security-Policy:
default-src 'self';
frame-src 'self' https://paperform.co https://*.paperform.co;
child-src 'self' https://paperform.co https://*.paperform.co;
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self';
font-src 'self' https: data:;
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
object-src 'none';
Why this works
frame-srcallows loading the Paperform iframechild-srcis there for older browser behavior and belt-and-suspenders compatibilityimg-src https:avoids random broken icons and remote imagesstyle-src 'unsafe-inline'is sometimes necessary if your own frontend injects inline styles around embeds
If your site doesn’t use inline styles, remove 'unsafe-inline'.
CSP for the Paperform embed script
If you load Paperform’s JavaScript embed helper, allow the script origin too:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://paperform.co https://*.paperform.co;
frame-src 'self' https://paperform.co https://*.paperform.co;
child-src 'self' https://paperform.co https://*.paperform.co;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://paperform.co https://*.paperform.co;
font-src 'self' https: data:;
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
object-src 'none';
I’d only add connect-src for Paperform if your browser console shows blocked XHR or fetch requests. Don’t cargo-cult extra hosts into a policy unless you actually need them.
If you use a custom Paperform domain
A lot of teams use branded form URLs like:
https://forms.example.comhttps://apply.example.com
When that happens, your CSP must allow your custom Paperform host, not just paperform.co.
Example:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://forms.example.com;
frame-src 'self' https://forms.example.com;
child-src 'self' https://forms.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://forms.example.com;
font-src 'self' https: data:;
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
object-src 'none';
If the embed script still comes from paperform.co but the form itself loads from forms.example.com, allow both:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://paperform.co https://forms.example.com;
frame-src 'self' https://paperform.co https://forms.example.com;
child-src 'self' https://paperform.co https://forms.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://paperform.co https://forms.example.com;
font-src 'self' https: data:;
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
object-src 'none';
A stricter modern policy
If your app already uses nonces and you want a tighter CSP, this is a better shape:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m123' 'strict-dynamic' https://paperform.co;
frame-src 'self' https://paperform.co https://*.paperform.co;
style-src 'self';
img-src 'self' data: https:;
connect-src 'self' https://paperform.co https://*.paperform.co;
font-src 'self';
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
object-src 'none';
report-to default-endpoint;
report-uri https://csp-report.example.com/report;
This only makes sense if your app is already built around nonces. If not, don’t fake it. A broken nonce rollout is worse than a simple allowlist.
For more ready-made patterns, csp-examples.com is handy.
Nginx example
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' https://paperform.co https://*.paperform.co;
frame-src 'self' https://paperform.co https://*.paperform.co;
child-src 'self' https://paperform.co https://*.paperform.co;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://paperform.co https://*.paperform.co;
font-src 'self' https: data:;
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
object-src 'none';
" always;
Apache example
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://paperform.co https://*.paperform.co; frame-src 'self' https://paperform.co https://*.paperform.co; child-src 'self' https://paperform.co https://*.paperform.co; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://paperform.co https://*.paperform.co; font-src 'self' https: data:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none';"
Express / Node example
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
[
"default-src 'self'",
"script-src 'self' https://paperform.co https://*.paperform.co",
"frame-src 'self' https://paperform.co https://*.paperform.co",
"child-src 'self' https://paperform.co https://*.paperform.co",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"connect-src 'self' https://paperform.co https://*.paperform.co",
"font-src 'self' https: data:",
"frame-ancestors 'self'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'"
].join("; ")
);
next();
});
Debugging blocked Paperform embeds
Open DevTools and look for messages like:
Refused to frame ... because it violates the following Content Security Policy directive: "frame-src ..."Refused to load the script ... because it violates "script-src ..."Refused to connect to ... because it violates "connect-src ..."
That error tells you exactly which directive is wrong.
I usually test in this order:
- Add only
frame-src - If using embed JS, add
script-src - If network calls fail, add
connect-src - Tighten everything else after it works
That’s faster than pasting a giant permissive policy and pretending it’s secure.
If you want to inspect your live header and catch obvious mistakes, headertest.com is useful for a quick sanity check. A real-world CSP from the site looks like this:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-MTE2N2MzNzQtOGFhMy00ZGYzLTgxYTgtM2Q3ZmNlNGMxMGJl' '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'
That’s a good example of a CSP that grew around actual dependencies instead of wishful thinking.
Report-Only first if you can
For production sites, I prefer rolling out Paperform CSP changes with Content-Security-Policy-Report-Only first:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://paperform.co https://*.paperform.co;
frame-src 'self' https://paperform.co https://*.paperform.co;
connect-src 'self' https://paperform.co https://*.paperform.co;
report-uri https://csp-report.example.com/report;
That gives you violation reports without breaking the form immediately.
Common mistakes
Using default-src and assuming it covers everything
It’s the fallback, not magic. If you define script-src, frame-src, or connect-src, those specific directives take over.
Forgetting frame-src
This is the classic one for Paperform iframe embeds.
Allowing https: everywhere
It works, but it’s lazy. Fine for img-src sometimes. Bad idea for script-src.
Leaving object-src unset
Set it to 'none'. There’s almost never a reason not to.
Confusing frame-ancestors with frame-src
frame-src= what your page can loadframe-ancestors= who can embed your page
Different problem, different directive.
Safe starting policies
1. Plain iframe embed
Content-Security-Policy:
default-src 'self';
frame-src 'self' https://paperform.co https://*.paperform.co;
child-src 'self' https://paperform.co https://*.paperform.co;
img-src 'self' data: https:;
style-src 'self';
script-src 'self';
connect-src 'self';
font-src 'self';
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
object-src 'none';
2. Embed script + iframe
Content-Security-Policy:
default-src 'self';
script-src 'self' https://paperform.co https://*.paperform.co;
frame-src 'self' https://paperform.co https://*.paperform.co;
child-src 'self' https://paperform.co https://*.paperform.co;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://paperform.co https://*.paperform.co;
font-src 'self' https: data:;
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
object-src 'none';
3. Custom Paperform domain
Content-Security-Policy:
default-src 'self';
script-src 'self' https://forms.example.com;
frame-src 'self' https://forms.example.com;
child-src 'self' https://forms.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://forms.example.com;
font-src 'self' https: data:;
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
object-src 'none';
If you remember one thing, make it this: start narrow, watch the console, and only add the exact Paperform origins your integration actually uses. That’s how CSP stays useful instead of turning into decorative security.