AWeber form embeds are the kind of thing teams paste into production on a Friday afternoon and only think about again when signups mysteriously stop working.

I’ve seen this pattern a lot: the site starts with a pretty solid Content Security Policy, marketing drops in an email signup form, and suddenly the browser console fills with CSP violations. The usual reaction is to loosen the policy until the errors disappear. That works, but it also trashes the point of having CSP in the first place.

Here’s a better way to handle it.

The setup

A developer-owned marketing site had a reasonably strict CSP. The baseline looked a lot like this real-world header seen on headertest.com:

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-MjcyMTIzNDgtMjRlYi00Mjk1LTg2NTEtYTZhYTYyMDViYTM5' '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 not bad. It has a nonce, uses strict-dynamic, blocks plugins with object-src 'none', and locks down frame-ancestors and base-uri.

Then marketing added an AWeber signup form.

What broke

Two things usually happen with AWeber forms:

  1. The browser blocks the form submission because form-action 'self' does exactly what it says.
  2. If you use AWeber’s JavaScript embed instead of a plain HTML form, scripts, styles, frames, or images from AWeber domains get blocked too.

The team first used a hosted form embed that looked roughly like this:

<script src="https://forms.aweber.com/form/12/123456789.js"></script>

And the console errors started immediately:

Refused to load the script 'https://forms.aweber.com/form/12/123456789.js'
because it violates the following Content Security Policy directive:
"script-src 'self' 'nonce-...' 'strict-dynamic' https://www.googletagmanager.com ..."

Then they switched to a plain HTML form, which looked safer:

<form method="post" action="https://www.aweber.com/scripts/addlead.pl">
  <input type="email" name="email" required>
  <input type="submit" value="Subscribe">
</form>

That failed too:

Refused to send form data to 'https://www.aweber.com/scripts/addlead.pl'
because it violates the following Content Security Policy directive:
"form-action 'self'"

This is the part where people get sloppy and throw https: into everything.

Don’t do that.

The bad fix

I’ve inherited policies like this more times than I want to admit:

Content-Security-Policy:
  default-src 'self' https:;
  script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
  style-src 'self' 'unsafe-inline' https:;
  img-src 'self' data: https:;
  connect-src 'self' https: wss:;
  frame-src https:;
  form-action 'self' https:;

Yes, the AWeber form works. So does half the ad-tech ecosystem, any random third-party script someone sneaks in, and a bunch of XSS payloads that your old policy would have stopped.

A CSP that says “allow basically every HTTPS origin” is security theater.

The right fix

First, decide which AWeber integration you actually need.

There are two common cases:

Case 1: Plain HTML form posting to AWeber

This is the cleaner option. If you can avoid third-party JavaScript, do it. Usually the only CSP change you need is form-action, and sometimes img-src if you use a hosted logo or tracking image.

Case 2: AWeber JavaScript embed

This usually needs updates to script-src, maybe frame-src, maybe style-src, and sometimes connect-src or img-src depending on what the embed loads.

My default advice: prefer the plain form unless there’s a hard product requirement for the script embed.

Before: existing production policy

Here’s the original policy, simplified for readability:

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-rAnd0m' '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';

After: minimal change for a plain AWeber form

If your page uses a direct HTML form post to AWeber, the smallest safe change is usually this:

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

That’s it. One extra origin in form-action.

And the form HTML:

<form
  method="post"
  action="https://www.aweber.com/scripts/addlead.pl"
  accept-charset="utf-8"
>
  <label for="email">Email</label>
  <input id="email" type="email" name="email" required>

  <input type="hidden" name="listname" value="your_list_id">
  <input type="hidden" name="redirect" value="https://example.com/thanks/">

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

If this is all you need, stop here. Don’t add AWeber to script-src just because someone said “forms need scripts.” They often don’t.

After: policy for an AWeber script embed

If you must use their hosted JavaScript form, add only the origins the embed actually needs. A realistic first pass looks like this:

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-rAnd0m' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://forms.aweber.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://www.aweber.com;
  object-src 'none';

And the embed:

<script
  nonce="{{ .CSPNonce }}"
  src="https://forms.aweber.com/form/12/123456789.js">
</script>

A quick note on strict-dynamic: once you trust a nonce-bearing script, that script can load other scripts. That’s useful, but it also means you should be deliberate about which initial scripts get your nonce. Don’t slap a nonce on every script tag out of habit.

What we found in testing

When we tested this in Content-Security-Policy-Report-Only first, the browser reports told us exactly what was still missing.

Example report for blocked form submission:

{
  "csp-report": {
    "document-uri": "https://example.com/newsletter/",
    "violated-directive": "form-action",
    "blocked-uri": "https://www.aweber.com/scripts/addlead.pl"
  }
}

Example report for blocked embed script:

{
  "csp-report": {
    "document-uri": "https://example.com/newsletter/",
    "violated-directive": "script-src-elem",
    "blocked-uri": "https://forms.aweber.com/form/12/123456789.js"
  }
}

That’s why I like rolling out third-party changes in report-only mode first. You get facts instead of guesses.

A couple of gotchas

form-action is separate from connect-src

People mix these up all the time.

connect-src controls fetch, XHR, WebSocket, Beacon, and similar browser APIs. A regular HTML form submit is governed by form-action. If your AWeber form is a standard <form> post, changing connect-src won’t fix anything.

default-src is not a shortcut for everything

Some developers assume that because default-src includes a host, forms and frames will magically work. Nope. Specific directives like form-action, frame-src, and script-src take precedence.

Don’t widen frame-src unless you actually use iframes

Some hosted email widgets use iframes. Some don’t. Check the network and DOM output before adding AWeber to frame-src.

If I were shipping this on a developer-focused site today, I’d use one of these two patterns.

Safer option: plain HTML form

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM}}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  style-src 'self' 'unsafe-inline' https://*.cookiebot.com https://consent.cookiebot.com;
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
  frame-src 'self' https://consentcdn.cookiebot.com;
  form-action 'self' https://www.aweber.com;
  frame-ancestors 'none';
  base-uri 'self';
  object-src 'none';

Script embed option

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM}}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://forms.aweber.com;
  style-src 'self' 'unsafe-inline' https://*.cookiebot.com https://consent.cookiebot.com;
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
  frame-src 'self' https://consentcdn.cookiebot.com;
  form-action 'self' https://www.aweber.com;
  frame-ancestors 'none';
  base-uri 'self';
  object-src 'none';

If you want a starting template for these patterns, the policy examples at https://csp-examples.com are handy for adapting to your stack.

The result

After the CSP change:

  • the AWeber signup form submitted successfully
  • no broad https: allowlists were added
  • existing protections stayed intact
  • the team had a documented reason for every third-party origin in the policy

That last bit matters more than people think. Six months later, when someone asks why forms.aweber.com is in script-src, you want an answer better than “because otherwise marketing got mad.”

For the directive details, the official CSP docs on MDN are still the best reference when you need exact browser behavior: https://developer.mozilla.org/docs/Web/HTTP/CSP

My rule of thumb is simple: if AWeber only needs to receive the form post, give it form-action and nothing else. Every extra directive should be justified by an observed request, not a guess. That’s how you keep CSP useful instead of turning it into a giant exception list.