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-srcfor the loaderconnect-srcfor API callsimg-srcfor stars/logos/avatarsframe-srcif it embeds an iframestyle-srcif it injects stylesfont-srcif 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-srcfor the iframe origin- maybe
script-srctoo if you use a bootstrap script img-srcif 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
Footer trust badge
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:
- Start from a locked-down baseline.
- Add the vendor script with a nonce if possible.
- Load the page and inspect:
- blocked scripts
- blocked XHR/fetch
- blocked images
- blocked frames
- blocked fonts
- Add only the exact hosts you observed.
- Prefer exact origins over wildcards.
- Keep
object-src 'none',base-uri 'self', andframe-ancestors 'none'unless you have a real reason not to. - 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.