Trust badges and review widgets are classic CSP troublemakers.

They look harmless: a tiny badge, a star rating, maybe a “verified reviews” block in the footer. Then you add one script and suddenly you need script-src, frame-src, img-src, style-src, and connect-src exceptions across half the internet.

I’ve cleaned this up on enough production sites to have a strong opinion: treat every badge or review widget like a third-party app, not a decoration.

What these widgets usually need

Most trust badges and review widgets load some mix of:

  • JavaScript from a vendor CDN
  • images for stars, logos, or reviewer avatars
  • inline styles or injected <style> blocks
  • XHR/fetch calls to review APIs
  • iframes for embedded widgets
  • fonts from the vendor or a shared CDN

If you only whitelist the script host, the widget often still breaks.

A minimal mental checklist:

  • script-src for the loader
  • connect-src for API calls
  • img-src for stars/logos/avatars
  • frame-src if it embeds an iframe
  • style-src if it injects styles
  • font-src if custom fonts are used

If you want directive-by-directive background, the official reference is here: MDN CSP. For a practical breakdown of directives, csp-guide.com is useful.

Start from a sane baseline

Here’s a baseline policy I’d actually ship and then extend for a widget:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m123';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  frame-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

That baseline is intentionally tight. Most widget integrations will fail under it, which is exactly what you want during setup.

Pattern 1: script + API + images

A lot of review vendors work like this:

  • you include a script from widget.vendor.example
  • the script fetches review data from api.vendor.example
  • it renders stars and logos from cdn.vendor.example

Example:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m123' https://widget.vendor.example;
  connect-src 'self' https://api.vendor.example;
  img-src 'self' data: https://cdn.vendor.example;
  style-src 'self';
  font-src 'self';
  frame-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

And the page:

<script nonce="rAnd0m123" src="https://widget.vendor.example/reviews.js"></script>
<div id="review-widget" data-business-id="abc123"></div>

This is the cleanest integration style. No iframe, no wildcard, no unsafe-inline.

Pattern 2: iframe-based trust badge

Some “verified site” badges are just iframes. That changes the CSP shape.

You’ll usually need:

  • frame-src for the iframe origin
  • maybe script-src too if you use a bootstrap script
  • img-src if fallback assets load outside the frame

Example:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data:;
  connect-src 'self';
  frame-src 'self' https://badge.vendor.example;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Markup:

<iframe
  src="https://badge.vendor.example/embed/site/12345"
  title="Trusted site badge"
  width="140"
  height="80"
  loading="lazy">
</iframe>

If the vendor tells you to allow child-src, that’s usually outdated advice. Use frame-src. child-src is legacy and mostly there for backward compatibility.

Pattern 3: vendors that require inline styles

This is where things get annoying.

A lot of widgets inject inline styles or ask you to add a <style> block directly into the page. If you can avoid that vendor, I would. But if you can’t, you may need one of these:

Option A: allow specific hashes

Best when the inline style block is static.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m123' https://widget.vendor.example;
  style-src 'self' 'sha256-AbCdEf1234567890examplehashhere=' https://widget.vendor.example;
  img-src 'self' data: https://cdn.vendor.example;
  connect-src 'self' https://api.vendor.example;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Option B: reluctantly use unsafe-inline for styles

I only do this when the widget is business-critical and there’s no practical alternative.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m123' https://widget.vendor.example;
  style-src 'self' 'unsafe-inline' https://widget.vendor.example;
  img-src 'self' data: https://cdn.vendor.example;
  connect-src 'self' https://api.vendor.example;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

If you’re going to weaken CSP, weaken style-src before script-src. Don’t casually add unsafe-inline to scripts.

Pattern 4: vendors that chain-load more scripts

Some widget bootstraps load another script dynamically. If you use nonces, strict-dynamic can help, but only if you understand what it does.

The real-world header from headertest.com is a good example:

content-security-policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-YWM3NGIyMjgtYjVmOS00Yzg4LWFmMDktZjBkMGVjNDA1NzYx' '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://u.headertest.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'

For trust widgets, a similar pattern might look like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m123' 'strict-dynamic' https://widget.vendor.example;
  connect-src 'self' https://api.vendor.example;
  img-src 'self' data: https://cdn.vendor.example;
  style-src 'self' https://widget.vendor.example;
  frame-src 'self' https://embed.vendor.example;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Use strict-dynamic when you trust the nonce-bearing bootstrap script to load other scripts. Don’t throw it in because a blog post said it was “modern”.

Copy-paste examples by widget type

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://badge.vendor.example;
  img-src 'self' data: https://badge.vendor.example https://cdn.vendor.example;
  style-src 'self';
  connect-src 'self' https://api.vendor.example;
  frame-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Product page review stars

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m123' https://reviews.vendor.example;
  connect-src 'self' https://api.reviews.vendor.example;
  img-src 'self' data: https://assets.vendor.example;
  style-src 'self' https://reviews.vendor.example;
  font-src 'self' https://assets.vendor.example;
  frame-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Full review widget in iframe

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data:;
  connect-src 'self';
  frame-src 'self' https://reviews.vendor.example;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Review widget with WebSocket updates

A few “live social proof” widgets open WebSockets.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://widget.vendor.example;
  connect-src 'self' https://api.vendor.example wss://stream.vendor.example;
  img-src 'self' data: https://cdn.vendor.example;
  style-src 'self' 'unsafe-inline';
  frame-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

If the widget silently fails after initial render, check connect-src for missing wss: hosts.

Common mistakes

1. Stuffing everything into default-src

This works badly with third-party widgets. Be explicit.

Bad:

Content-Security-Policy: default-src 'self' https://vendor.example

Better:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://vendor.example;
  connect-src 'self' https://api.vendor.example;
  img-src 'self' https://cdn.vendor.example;

2. Allowing whole wildcards too early

Bad:

script-src 'self' https://*.vendor.example;
connect-src 'self' https://*.vendor.example;
img-src 'self' https:;

Sometimes you need wildcards. Usually you don’t need them on day one. Start with exact hosts from violation reports and network traces.

3. Forgetting img-src

This one wastes a lot of time. The widget script loads, API calls succeed, but star icons or trust logos are broken. That’s almost always img-src.

4. Missing frame-src

If the vendor uses an iframe and you only allow its script host, the embed still won’t render.

5. Reaching for unsafe-inline in script-src

Don’t. Use nonces or hashes. Official docs for that are here: MDN script-src.

Roll out with Report-Only first

For third-party widgets, Content-Security-Policy-Report-Only saves a lot of guesswork.

Example:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m123' https://reviews.vendor.example;
  connect-src 'self' https://api.reviews.vendor.example;
  img-src 'self' data: https://assets.vendor.example;
  style-src 'self' https://reviews.vendor.example;
  report-to csp-endpoint;

Or with the older reporting directive:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' https://reviews.vendor.example;
  report-uri /csp-report;

I usually deploy the widget in staging, open DevTools, watch the network tab, then compare that with CSP violations. Vendors routinely forget to document one or two hosts.

A practical integration workflow

This is the process I use:

  1. Start from a locked-down baseline.
  2. Add the vendor script with a nonce if possible.
  3. Load the page and inspect:
    • blocked scripts
    • blocked XHR/fetch
    • blocked images
    • blocked frames
    • blocked fonts
  4. Add only the exact hosts you observed.
  5. Prefer exact origins over wildcards.
  6. Keep object-src 'none', base-uri 'self', and frame-ancestors 'none' unless you have a real reason not to.
  7. Use Report-Only before enforcing.

That last point matters. Trust badges are often dropped into marketing pages by people who don’t want to hear that “security broke the stars again.” Report-Only gives you room to get it right without taking the blame for a rollout surprise.

A production-ready example

Here’s a realistic policy for a site using one review widget, one badge iframe, and standard app assets:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m123' https://reviews.vendor.example;
  style-src 'self' https://reviews.vendor.example;
  img-src 'self' data: https://assets.vendor.example https://badge.vendor.example;
  font-src 'self' https://assets.vendor.example;
  connect-src 'self' https://api.reviews.vendor.example;
  frame-src 'self' https://badge.vendor.example;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

That’s the shape you want: narrow, readable, and obviously tied to actual widget behavior.

If your trust badge or review widget needs a policy twice as long as this, I’d question the vendor before I loosen the header. Third-party marketing widgets have a habit of asking for too much. CSP is where you push back.