I’ve seen this one more than once: a team adds a Mailchimp signup form late in the launch cycle, everything works in staging, then production CSP blocks half of it.

The usual reaction is predictable: someone pastes *.mailchimp.com into default-src, sprinkles in 'unsafe-inline', and calls it done. The form works, but the policy gets weaker, noisier, and harder to maintain.

A better approach is to treat Mailchimp like any other third-party integration: measure what it actually needs, scope directives tightly, and avoid opening the whole page just to get one form working.

Here’s a real-world style case study of how I’d fix it.

The setup

A marketing team wanted a newsletter signup in the site footer and on a landing page. They used a Mailchimp embedded form, copied straight from the Mailchimp admin UI.

The site already had a decent CSP. It looked a lot like the one from HeaderTest:

content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-YjUwNmM5NzQtZGVhMS00MGVhLWFkMDUtNWU5N2QyMWJmZTQ1' '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 already pretty disciplined. The problem is obvious, though: form-action 'self' blocks posting to Mailchimp, and the default policy doesn’t allow Mailchimp assets.

What Mailchimp embed code usually brings with it

A standard Mailchimp embed often includes:

  • A <form> posting to a Mailchimp list endpoint
  • Hidden anti-bot fields
  • External CSS from cdn-images.mailchimp.com
  • Sometimes external JavaScript for validation
  • Inline styles or inline scripts, depending on which embed variant was copied

Typical embed code looks something like this:

<link
  href="//cdn-images.mailchimp.com/embedcode/classic-061523.css"
  rel="stylesheet"
  type="text/css">

<div id="mc_embed_signup">
  <form
    action="https://example.us1.list-manage.com/subscribe/post?u=abc123&id=def456"
    method="post"
    target="_blank"
    novalidate>
    <div>
      <label for="mce-EMAIL">Subscribe</label>
      <input type="email" name="EMAIL" id="mce-EMAIL" required>
      <input type="submit" value="Subscribe" name="subscribe">
      <div style="position:absolute; left:-5000px;" aria-hidden="true">
        <input type="text" name="b_abc123_def456" tabindex="-1" value="">
      </div>
    </div>
  </form>
</div>
<script src="//s3.amazonaws.com/downloads.mailchimp.com/js/mc-validate.js"></script>

From a CSP perspective, this is messy:

  • external stylesheet
  • external script
  • inline style attribute
  • cross-origin form submission
  • target="_blank" flow that may hide breakage during testing

Before: the lazy fix

The first pass I usually see looks like this:

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

Yes, the form probably works.

No, I would not ship this.

Problems:

  1. https: in multiple directives effectively trusts a huge chunk of the internet.
  2. 'unsafe-inline' in script-src is doing way too much damage for one signup form.
  3. 'unsafe-eval' is almost never justified here.
  4. The policy says nothing precise about which Mailchimp hosts are really needed.
  5. It weakens the entire page, not just the form.

This is the classic “make browser errors disappear” CSP.

How I debug this properly

I start with the existing policy in Content-Security-Policy-Report-Only, load the page, submit the form, and collect actual violations.

For a Mailchimp embed, I usually see some combination of these:

  • blocked form submission to https://<dc>.list-manage.com
  • blocked stylesheet from https://cdn-images.mailchimp.com
  • blocked script from https://s3.amazonaws.com/downloads.mailchimp.com/js/mc-validate.js
  • blocked inline style from the hidden honeypot field
  • sometimes blocked images if the form branding pulls remote assets

That tells me whether I should support the stock embed or replace parts of it.

My opinion: don’t blindly support the stock embed if it drags in weak CSP requirements. Trim it.

The better fix: keep the form, drop the junk

In this case, we replaced the Mailchimp-generated embed with a plain HTML form that posts directly to Mailchimp, but we hosted our own CSS and skipped Mailchimp’s validation script.

After: cleaned-up form markup

<section class="newsletter-signup">
  <h2>Get product updates</h2>

  <form
    action="https://example.us1.list-manage.com/subscribe/post?u=abc123&id=def456"
    method="post"
    class="signup-form"
    novalidate>
    <label for="email">Email address</label>
    <input
      id="email"
      name="EMAIL"
      type="email"
      autocomplete="email"
      required>

    <button type="submit">Subscribe</button>

    <div class="sr-only" aria-hidden="true">
      <label for="company">Company</label>
      <input
        id="company"
        type="text"
        name="b_abc123_def456"
        tabindex="-1"
        autocomplete="off">
    </div>
  </form>
</section>

And the CSS is local:

.newsletter-signup {
  max-width: 28rem;
}

.signup-form {
  display: grid;
  gap: 0.75rem;
}

.sr-only {
  position: absolute;
  left: -5000px;
}

This gets rid of two common CSP headaches immediately:

  • no external Mailchimp stylesheet
  • no Mailchimp validation script

That leaves one thing to allow: the form post itself.

After: tight CSP update

Starting from the existing site policy, the actual change was small:

Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-YjUwNmM5NzQtZGVhMS00MGVhLWFkMDUtNWU5N2QyMWJmZTQ1' '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' https://example.us1.list-manage.com;
object-src 'none'

That’s it.

The key decision was to scope form-action to the exact Mailchimp endpoint host instead of widening half the policy.

If your audience spans multiple Mailchimp data centers, you may need something broader, for example:

form-action 'self' https://*.list-manage.com;

I’d still keep that relaxation isolated to form-action. Don’t dump Mailchimp into default-src just because the browser suggested a violation.

What if you must use Mailchimp’s hosted assets?

Sometimes marketing insists on the stock form because they want Mailchimp’s exact styles or validation behavior. Fine. You can still do it without turning CSP into soup.

A more targeted policy might look like this:

Content-Security-Policy:
default-src 'self';
script-src 'self' https://s3.amazonaws.com;
style-src 'self' https://cdn-images.mailchimp.com;
img-src 'self' data: https:;
form-action 'self' https://*.list-manage.com;
base-uri 'self';
frame-ancestors 'none';
object-src 'none';

But watch for inline requirements. The stock embed often pushes you toward inline styles, and sometimes toward inline scripts if you paste their full snippet untouched.

My preference is still to rewrite the markup and keep the integration dumb: HTML form, server-agnostic, no third-party JS.

The result

After the cleanup:

  • the signup form worked reliably
  • CSP stayed strict
  • no new script exceptions were added
  • no https: wildcards
  • no 'unsafe-eval'
  • no Mailchimp assets loaded except the actual form submission endpoint

That last part matters. A newsletter form should not force you to trust a random S3-hosted script on every page.

A couple of practical gotchas

1. form-action is the directive that actually matters

People often add a domain to connect-src and wonder why form submits still fail. Browser form posts are governed by form-action, not connect-src.

2. Embedded forms can hide CSP failures

If the form submits in a new tab or redirects to a Mailchimp confirmation page, some teams miss the fact that assets on the original page were blocked. Always check DevTools console and network logs.

3. Mailchimp hostnames vary

You’ll usually see endpoints like:

  • us1.list-manage.com
  • us6.list-manage.com

Use the exact host if you can. Wildcard only when you need it.

4. Don’t cargo-cult 'unsafe-inline'

If Mailchimp’s embed includes inline styling, that doesn’t mean your whole app needs permissive inline script policy. Move what you can to local CSS. Be stubborn here.

A sane baseline policy for Mailchimp forms

If you’re adding a plain HTML Mailchimp form to an otherwise locked-down site, this is the shape I recommend:

Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-random123' 'strict-dynamic';
style-src 'self';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
form-action 'self' https://*.list-manage.com;
base-uri 'self';
frame-ancestors 'none';
object-src 'none';

Adjust for your own analytics, consent tools, and app APIs, of course. If you want more ready-made patterns, csp-examples.com is a useful starting point.

My rule of thumb is simple: Mailchimp should expand form-action, not bulldoze your whole CSP.

If a signup form requires broad script-src and style-src exceptions, I treat that as a sign to rewrite the embed, not weaken the site. That choice usually takes less time than cleaning up a bloated policy six months later.