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:

  1. Iframe-only embed

    • Easiest to secure
    • Least page integration
    • Usually best for strict CSP setups
  2. Tally embed script

    • Better UX and auto-resizing
    • Requires allowing Tally script sources
    • More moving parts in CSP
  3. 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-src
  • script-src
  • connect-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-src
  • style-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:

  1. Start with iframe-only
  2. Add only:
    • frame-src https://tally.so
  3. Test
  4. If the UX is not good enough, switch to the embed script
  5. Add only the exact directives needed from browser violation reports
  6. 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.

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.