Content Security Policy (CSP) is one of the most effective browser-security standards available today. When properly deployed, a CSP acts as a robust defense layer against common web vulnerabilities, including Cross-Site Scripting (XSS), clickjacking, and packet-injection attacks. By explicitly specifying which dynamic resources are authorized to execute on a page, CSP restricts the damage an attacker can inflict even if they find a vulnerability in your code.
However, almost no modern site runs entirely on first-party code. Analytics, embedded media, maps, payment widgets, tag managers, and advertising scripts all load from third-party origins — and each one is both a feature and a potential attack surface. These integrations are exactly where a CSP earns its keep, and also where it is most often misconfigured. Because many of them load resources dynamically from rotating domains, a policy that is too rigid will block legitimate scripts, leaving broken widgets, empty slots, and a console full of violations.
the challenge of dynamic third-party scripts
Many third-party integrations — ad tags, tag managers, A/B testing tools, analytics — do not load a single static file. They rely on a nested chain of script loaders that fetch further scripts from additional domains at runtime. Google's ad and tag scripts are a representative example: an initial loader evaluates the page and then pulls assets from various distributed Google domains. If your site enforces a generic policy such as default-src 'self', the browser refuses every external origin, the loader is blocked immediately, and you are left with console errors and empty blocks where content should be.
The job, then, is to coordinate your CSP directives so they grant the specific origins your integrations genuinely need — and nothing more — while keeping everything else locked down. There are two ways to do that, and which one you choose depends on whether your pages are rendered dynamically or served statically.
the gold standard: strict CSP with nonces
For dynamically rendered pages, the strongest approach is a strict CSP built around cryptographic nonces (number used once). A nonce is a random, mathematically secure string generated on the server for each page load. By declaring this nonce in your CSP header and attaching it to your trusted <script> tags, the browser can verify that a script is explicitly sanctioned by the server — and reject any injected script that lacks the secret.
The standard strict CSP structure looks like this:
Content-Security-Policy:
object-src 'none';
script-src 'nonce-{random-value}' 'strict-dynamic' https: 'unsafe-inline';
base-uri 'none';
Crucially, the 'strict-dynamic' directive acts as a bridge. It tells the browser that any script carrying a valid nonce is trusted to dynamically create and append further scripts. Since many third-party loaders — ad tags and tag managers among them — rely heavily on dynamic script insertion, this lets the trusted top-level script load its auxiliary files without forcing you to maintain a massive, brittle allowlist of downstream domains. (Modern browsers honour 'strict-dynamic' and ignore the https: and 'unsafe-inline' fallbacks, which exist only for older engines.)
configuring CSP for static hosting environments
Strict nonces are the ideal choice for dynamic applications, but they are difficult to implement on static hosts like Firebase Hosting, GitHub Pages, or Netlify, where HTML is served straight from edge servers and cannot execute per-request code to generate a fresh nonce.
On static platforms, you fall back to a carefully tuned allowlist approach, declaring the trusted third-party origins (here, Google's ad and tag hosts) across the relevant directives in your firebase.json header configuration:
- script-src: Allows the execution of scripts from Google's script hosts. We must include
https://*.google.com,https://*.googlesyndication.com,https://*.googletagservices.com,https://*.gstatic.com, andhttps://*.doubleclick.net. - frame-src: Enables rendering the actual ad units in nested iframes, utilizing
https://*.google.com,https://*.googlesyndication.com, andhttps://*.doubleclick.net. - connect-src: Permits the loaders to send XMLHttpRequest/Fetch requests to server-side endpoints, requiring
https://*.google.com,https://*.googlesyndication.com, andhttps://*.doubleclick.net. - img-src: Allows image assets to load, requiring
https://*.google.com,https://*.googlesyndication.com,https://*.doubleclick.net, andhttps://*.gstatic.com.
By defining these parameters, you establish a solid boundary that safeguards your users from rogue injections while giving legitimate third-party scripts the technical clearance they need to run reliably. Done well, it also prevents the layout shifts and blank spaces that hurt Core Web Vitals when a blocked resource fails to fill its slot.
roll out safely with report-only mode
The fastest way to break a production site is to ship a strict CSP that you have not tested against real traffic. A single missing directive can silently block a critical script. This is exactly what Content-Security-Policy-Report-Only is for. Delivered as a separate header, it instructs the browser to evaluate your policy and report every violation it would have blocked — without actually blocking anything.
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://*.googlesyndication.com;
report-to csp-endpoint;
Run the report-only policy in parallel with your live site for a week or two, collect the violation reports through the report-to directive, and you will quickly see which legitimate resources you forgot to allow. Once the reports go quiet, you can promote the same policy to the enforcing Content-Security-Policy header with confidence.
common pitfalls to avoid
A few mistakes account for most CSP headaches. The first is leaning on 'unsafe-inline' for scripts: it disables the core protection CSP provides, because an attacker who injects a script tag is no longer blocked. Prefer nonces or hashes, and reserve 'unsafe-inline' for styles only if you genuinely cannot refactor them. The second is forgetting that directives do not inherit from each other beyond default-src — if you specify script-src but a resource type has no matching directive, the browser falls back to default-src, which can produce surprising blocks. The third is overusing broad wildcards: https: as a source allows scripts from any HTTPS origin and defeats the purpose. Be as specific as your dependencies allow, and revisit the policy whenever you add a new third-party integration.
A well-tuned Content Security Policy is never quite "finished" — it is a living part of your infrastructure that evolves with your dependencies. Treated that way, it remains one of the highest-value, lowest-cost defences you can add to any website.