If you embed Tally forms on a site with a serious Content Security Policy, you’ll hit friction fast.
Tally is easy to drop into a page. CSP is not forgiving. That mismatch is where teams usually get stuck: the marketing team wants a form live in five minutes, and the security policy says “absolutely not” unless every source is accounted for.
Here’s the practical guide I wish more teams had before they started whitelisting random domains.
The short version
For Tally, you usually have three paths:
-
Iframe-only embed
- Easiest to secure
- Least page integration
- Usually best for strict CSP setups
-
Tally embed script
- Better UX and auto-resizing
- Requires allowing Tally script sources
- More moving parts in CSP
-
Relaxed CSP to “make it work”
- Fastest in the moment
- Worst long-term security posture
- Usually how CSP slowly turns into theater
My opinion: start with the iframe approach unless you truly need script-driven behavior.
What Tally changes in your CSP
Tally forms are usually embedded from Tally-controlled origins, and that affects at least:
frame-srcscript-srcconnect-src- sometimes
style-src - potentially
img-src
If you use their script embed instead of a plain iframe, your policy gets broader.
A lot of teams already have a CSP that looks like the real-world one below, taken from headertest.com:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-ODU4MjgzM2QtNzNlMi00NzY1LWI0ZTYtODc1YTBkODg5NDZj' '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 policy already allows analytics and consent tooling, but it will still block a Tally iframe unless you explicitly allow Tally in frame-src.
That’s the pattern I see all the time: teams assume default-src will cover them. It often won’t, because the more specific directive wins.
Option 1: Plain iframe embed
This is the cleanest option for CSP.
Example embed:
<iframe
src="https://tally.so/r/your-form-id"
width="100%"
height="500"
frameborder="0"
marginheight="0"
marginwidth="0"
title="Contact form">
</iframe>
CSP you’ll usually need
Content-Security-Policy:
default-src 'self';
frame-src 'self' https://tally.so;
img-src 'self' data: https:;
style-src 'self';
script-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Pros
- Smallest CSP change
- No third-party JavaScript on your page
- Easy to reason about
- Good fit for locked-down apps
Cons
- Less polished UX
- Height management can be clunky
- Less direct control over interactions
- Cross-frame customization is limited
If your security team is skeptical of third-party scripts, this is the version that usually gets approved.
Option 2: Tally embed script
This is the nicer developer experience and usually the nicer user experience too.
Typical pattern:
<div data-tally-src="https://tally.so/embed/your-form-id?alignLeft=1&hideTitle=1&transparentBackground=1"></div>
<script async src="https://tally.so/widgets/embed.js"></script>
Now you’re not just framing Tally. You’re running Tally JavaScript in your page context.
CSP you’ll usually need
Content-Security-Policy:
default-src 'self';
script-src 'self' https://tally.so;
frame-src 'self' https://tally.so;
connect-src 'self' https://tally.so;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Depending on how the widget behaves, you may need to allow additional Tally-controlled assets. Test with Content-Security-Policy-Report-Only first.
Pros
- Better sizing and embed behavior
- Less manual layout work
- Good for popups, slides, and richer embeds
- Closer to the “copy-paste and done” experience people want
Cons
- You trust third-party JS
- Broader CSP
- Harder to audit over time
- Can conflict with strict nonce/hash-based policies
This is where strict CSP setups start getting annoying. If your app relies on nonce-based scripts with 'strict-dynamic', a random third-party embed often means adding special handling or carving out exceptions.
Option 3: Nonce-based strict CSP with Tally exceptions
If you already run a modern strict CSP, don’t throw it away for one form.
Use targeted exceptions instead.
For example, if your current policy looks something like the headertest.com one, you might extend it carefully:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-rAnd0m123' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://tally.so;
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 https://tally.so;
frame-src 'self' https://consentcdn.cookiebot.com https://tally.so;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
Pros
- Preserves your existing CSP design
- Avoids opening the floodgates
- Works well in mature apps
Cons
- Requires actual testing
- Can be brittle if Tally changes asset origins
- Not as simple as “paste this one line”
This is the right path for production apps with a security baseline you care about.
Option 4: Overly broad allowlists
You’ve seen this one before:
Content-Security-Policy:
default-src * data: blob: 'unsafe-inline' 'unsafe-eval';
Yes, your form will probably work. Your CSP is also basically decorative now.
Pros
- Fast
- Low effort
- Reduces support tickets for a week
Cons
- Terrible security
- Makes future auditing harder
- Encourages more exceptions
- Defeats the point of CSP
I don’t recommend this unless your goal is to have a CSP header purely because someone’s compliance checklist asked for one.
The big decision: iframe vs script
Here’s the comparison I’d use in a real project.
| Approach | Security | UX | CSP complexity | Maintenance |
|---|---|---|---|---|
| Plain iframe | Stronger | Good enough | Low | Low |
| Embed script | Weaker | Better | Medium | Medium |
| Broad allowlist | Weak | Fine | Low now, high later | High |
If you’re building a docs site, marketing site, or support page, iframe wins most of the time.
If you need popup forms, dynamic resizing, or polished interactions, the embed script is defensible, but only with explicit source controls.
Common breakages and what they mean
Refused to frame ... because it violates frame-src
You forgot to allow Tally in frame-src.
frame-src 'self' https://tally.so;
Refused to load the script ... because it violates script-src
You’re using the widget script and didn’t allow the source.
script-src 'self' https://tally.so;
If you use a nonce-based setup, keep the nonce and add Tally only as needed.
Form renders badly or doesn’t auto-resize
This usually means the iframe works but the helper script doesn’t, or some style restriction is blocking expected behavior.
Check:
script-srcstyle-src- browser console CSP violations
Network calls fail after form submission
You may need connect-src entries for Tally-owned endpoints used by the embed.
Start narrow and expand only based on real violations.
A practical rollout plan
This is how I’d ship it:
- Start with iframe-only
- Add only:
frame-src https://tally.so
- Test
- If the UX is not good enough, switch to the embed script
- Add only the exact directives needed from browser violation reports
- Keep the final policy documented next to the embed code
For teams that want ready-made CSP patterns, CSP Examples is useful for comparing policy styles. For browser behavior and directive details, use the official CSP documentation from browser vendors and the CSP spec.
Recommended baseline policies
Best default for security-conscious teams
Content-Security-Policy:
default-src 'self';
frame-src 'self' https://tally.so;
img-src 'self' data: https:;
script-src 'self';
style-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Best default for Tally widget users
Content-Security-Policy:
default-src 'self';
script-src 'self' https://tally.so;
frame-src 'self' https://tally.so;
connect-src 'self' https://tally.so;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
My recommendation
If you want the safest answer: embed Tally with a plain iframe and allow only frame-src https://tally.so.
If you want the nicest UX: use the Tally embed script, but treat it like any other third-party JavaScript and scope your CSP tightly.
What I would not do is loosen an otherwise solid policy just because a form vendor wants to be “easy to embed.” That’s how CSP drifts from useful defense into cargo-cult config.