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
styleattribute - 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:
https:in multiple directives effectively trusts a huge chunk of the internet.'unsafe-inline'inscript-srcis doing way too much damage for one signup form.'unsafe-eval'is almost never justified here.- The policy says nothing precise about which Mailchimp hosts are really needed.
- 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.comus6.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.