# How do attackers bypass a Content Security Policy?

> Allowlisted CDNs that host arbitrary code, JSONP endpoints, open redirects, a missing base-uri and unsafe-eval are the ways a policy that looks strict gets walked around. What each bypass needs, how to close it, and why a successful bypass never appears in your violation reports.

- Canonical: https://info.centralcsp.com/articles/csp-bypass-techniques/
- Published: 2026-09-21
- Language: en
- Publisher: CentralCSP (https://centralcsp.com)

A Content Security Policy that looks strict in a config file and a Content Security Policy that stops an XSS are two different things. The gap between them is wide. Of the 761,345 domains in our State of the Web 2026 scan, 22.0% ship a CSP at all, and 0.97% ship one that actually passes. Most of the difference is not missing directives. It is directives that are present and walked around.

Every bypass below has the same shape: the attacker already has HTML injection, and the policy is the only thing standing between that and script execution. The bypass finds something the policy already permits and uses it as a loader. Nothing is exploited in the browser. The policy is simply obeyed.

That last point matters more than the techniques, so it is worth stating up front: a successful bypass produces no violation report. The browser allowed the script, because the policy said to.

## Allowlisted hosts that will run anything you give them

This is the big one, and it is why allowlist policies have a bad reputation.

```http
Content-Security-Policy: script-src 'self' https://cdn.example-libraries.net
```

Looks disciplined. One origin, a reputable CDN. The problem is that a public library CDN hosts thousands of files, and some of them will execute a string you control. An injected `<script src="https://cdn.example-libraries.net/some/old/framework.js">` is fully policy-compliant. If that old framework does client-side templating, the attacker has an expression evaluator running on your page with your origin's privileges.

The research is blunt about the scale. A Google team surveying real policies, published at ACM CCS in 2016, found the large majority of allowlist-based policies trivially bypassable, most often through exactly one entry in the list. Ten years later the finding holds, because the underlying cause has not changed: you do not control what a third-party origin serves, and the policy authorizes the origin rather than the file.

Same category, more mundane versions:

- A wildcard like `https://*.example.com` where one subdomain serves user uploads, a staging app or an old marketing site nobody maintains.
- Your own `'self'` origin, if it has an upload endpoint that will serve a file with a JavaScript content type, or an endpoint that reflects input into a `.js` response.
- A CDN account where any customer can upload, on a shared hostname.

The fix is not a better list. It is not having a list.

## JSONP endpoints, which are callback injection by design

A JSONP endpoint takes a callback name and wraps its JSON response in it:

```
https://api.allowed-vendor.example/data?callback=myHandler
```

That returns `myHandler({...})`, which is a script. Whatever you put in `callback` ends up as executable code at the top of the response. If `api.allowed-vendor.example` is in your `script-src`, an attacker injects a script tag pointing at it with a callback of their choosing, and the policy waves it through.

Plenty of vendors still expose these on origins that end up in analytics and advertising allowlists. Before trusting any origin in `script-src`, the question is not "do I trust this company" but "does anything on this hostname turn a query parameter into code". You usually cannot answer that, which is the argument for nonces again.

## Open redirects, and why path allowlists do not help

People try to tighten an allowlist by adding a path:

```http
Content-Security-Policy: script-src https://vendor.example/safe/scripts/
```

Then they find an open redirect on that host. CSP drops the path component when it follows a redirect, deliberately, so that policies do not leak redirect targets through error messages. So `https://vendor.example/safe/scripts/` matching a redirect that lands anywhere on an allowed origin defeats the path restriction entirely.

Path-based allowlisting buys you less than it appears to. Treat the origin as the unit of trust, because that is what the browser does in practice.

## The two directives people forget

`base-uri` is the quiet one. If an attacker can inject a `<base href="https://attacker.example">` tag, every relative script URL on your page resolves against their host instead of yours. Your own perfectly legitimate `<script src="/app/main.js">` now loads from them. A nonce does not save you here, because the tag that gets loaded is your tag, carrying your nonce.

Set it and stop thinking about it:

```http
base-uri 'none'
```

`object-src` is the other. Plugins are dead, but `<object>` and `<embed>` can still lead to script execution in some engines, and `default-src` does not cover the case as reliably as people assume. `object-src 'none'` costs nothing.

CentralCSP's Builder pins both to `'none'` in every policy it generates, for exactly this reason. They are free wins that show up as findings on otherwise respectable policies more often than anything else.

## unsafe-eval, and the libraries that need it

`'unsafe-eval'` re-enables `eval()`, `new Function()`, and string arguments to `setTimeout`. If it is in your policy, an injected payload that reaches any of those has full execution, whatever else the policy says about sources.

It is usually there because one library demanded it: a template engine, an old charting library, a tag manager configuration that builds functions from strings. That library is the policy. Whoever can influence the strings it compiles can run code.

If you cannot remove it immediately, at least know which dependency requires it and write the removal date next to it. In our experience the answer is often that the library was upgraded two years ago and no longer needs it, and nobody went back to check.

## Where strict-dynamic actually gets you

A nonce-based policy with `'strict-dynamic'` deletes the entire first half of this article:

```http
Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'
```

Once `'strict-dynamic'` is present, host-based allowlisting is disabled. Your CDN entries stop mattering, because they stop being consulted. A script that carries the correct nonce runs, scripts it loads programmatically inherit that trust, and an injected tag without the nonce does not run no matter which origin it points at. That is the shape you want.

It is not airtight, and the remaining failures are worth naming. A nonce that is reused across responses (static hosting, full-page CDN caching) is a password written on the door, which is the main reason [nonces and hashes are not interchangeable](/articles/csp-nonces-vs-hashes/). A nonce echoed into the page through a reflection is leaked. And a legitimate nonced script that passes attacker input into `innerHTML` or a template compiler is still a hole, because `'strict-dynamic'` trusts your script and your script trusted the attacker.

## The uncomfortable part: none of this shows up in reports

Go back to the opening point. A violation report is a record of something the browser refused. A bypass is something the browser permitted. The two sets do not overlap.

So a policy can be bypassed daily and the violation dashboard stays clean. Cleaner, in fact, than a site with a strict policy and some noisy legitimate breakage.

What does catch it is a record of what ran. CSP hash reporting (`'report-sha256'` in the policy) makes the browser report every script that executed, with its SHA-256, so you get an inventory drawn from real sessions rather than a crawl: every script, per page, first- and third-party, with hash history and first and last seen. A script served from an allowlisted CDN that nobody on your team recognises shows up there as a new hash on a page where it has no business being. That is the signal a violation stream cannot give you, and it is [how a script inventory gets built without an agent](/articles/script-inventory-without-an-agent/).

The pairing is the point. Violations tell you what your policy is blocking and where it is too tight. Script hashes tell you what your policy is allowing and where it is too loose. Running one without the other leaves you half blind, which is roughly where most CSP deployments sit.

## A short checklist

1. Run the [free CSP evaluator](https://centralcsp.com/en/tools/csp-evaluator/) against your current policy. It flags JSONP-prone origins, unsafe directives and missing `base-uri` or `object-src`, scored out of 100 with a fix per finding. It takes a minute and no account.
2. Set `object-src 'none'` and `base-uri 'none'` today. There is no migration and no risk.
3. Find out which dependency needs `'unsafe-eval'`. Check whether it still does.
4. Plan the move from allowlist to nonce plus `'strict-dynamic'`. Build it from real traffic rather than guessing, which is what [turning CSP reports into a policy](/articles/turn-csp-reports-into-policy/) covers.
5. Turn on script hash reporting so that the scripts your policy permits are inventoried, not assumed.

Step four is the one that takes weeks. Steps two and five take an afternoon between them and cover the failure modes that a strict-looking policy hides.

## Frequently asked questions

### Will a CSP bypass show up in my violation reports?

No, and this is the part that surprises people. A bypass works precisely because the injected script satisfies the policy, so the browser allows it and generates no violation. Violation reports show you what was blocked. To see what a bypass did, you need a record of what executed, which is what script hash reporting gives you: every script that ran, with its SHA-256.

### Is an allowlist-based CSP worth deploying at all?

It is better than nothing and much worse than a nonce-based policy. Google research presented at ACM CCS in 2016 found that the large majority of allowlist policies it surveyed were trivially bypassable, usually through one hostname in the list that served JSONP or arbitrary user content. If you already run an allowlist, keep it and plan the move to nonces with strict-dynamic rather than tuning the list.

### Does strict-dynamic make a policy bypass-proof?

It removes the whole category of allowlist bypasses, because host sources are ignored once it is present. It does not protect you from a nonce that leaks into the page, from unsafe-eval, or from a legitimate nonced script that loads attacker-controlled input. It is the strongest single thing you can add, not a finished answer.

### Why does object-src matter if nobody uses plugins?

Because default-src does not always cover it in the way people assume, and a permitted object or embed element can still execute script in some engines. It costs nothing to set object-src to none, there is no modern site that needs it, and leaving it unset is one of the most common findings on otherwise decent policies.
