Defending Against Content Security Policy (CSP) Bypass: Best Practices for JavaScript Applications

Defending Against Content Security Policy (CSP) Bypass: Best Practices for JavaScript Applications
Defending Against Content Security Policy (CSP) Bypass: Best Practices for JavaScript Applications

Explore how to prevent CSP bypass techniques in JavaScript applications. This tutorial walks you through real-world examples and provides actionable steps to improve your site's defenses.

You ship a Content Security Policy, see the header show up in DevTools, and feel safe. That feeling is the trap. A CSP that’s misconfigured or too permissive can be walked right around, and the attacker never trips a single alarm.

CSP is meant to stop Cross-Site Scripting (XSS) and other injection attacks by telling the browser exactly which resources are allowed to run. That’s a strong idea. But it only holds if the policy is tight. Here’s how attackers slip past weak CSPs, and how to write one that actually earns its place in your headers.

Table of Contents

What is Content Security Policy (CSP)?

Content Security Policy is an extra layer of defense for web apps. You give the browser an allowlist of trusted sources for scripts, styles, images, and media, and it refuses to load or run anything else. You set it with the Content-Security-Policy HTTP header.

Basic Example of a CSP Header
HTML
Content-Security-Policy: default-src 'self'; script-src 'self' https://apis.google.com

Here default-src 'self' limits resources to your own origin, while script-src lets scripts load from your origin plus Google’s API host.

Why CSP is Critical for Web Security

CSP’s job is to blunt injection attacks like XSS. Without one, an attacker who lands malicious JavaScript or HTML on your page can steal data, hijack sessions, or read whatever the user can. CSP narrows what’s allowed to execute, so a stray injected script has nowhere to run.

How Attackers Bypass CSP

A CSP is only as strong as its weakest directive. Misconfigure one line and the whole policy leaks. Here are the bypasses you’ll see most.

1. Using Unsafe Inline Directives

The classic mistake is switching on 'unsafe-inline'. It lets any inline script on the page run, which is the exact thing CSP exists to stop. Turn it on and you’ve mostly turned CSP off.

Example: Inline Script Vulnerability
JS
/**
 * This inline script example shows how using 'unsafe-inline' can allow XSS attacks.
 * @example
 * <script>alert('This could be injected code!');</script>
 */
HTML
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'

The header looks locked down, but 'unsafe-inline' waves through any inline script an attacker manages to inject. That’s your XSS hole, reopened.

2. Weak ‘script-src’ Directive

The script-src directive decides which scripts run. Point it at a domain you don’t fully control and you’ve handed that domain the keys. If an attacker compromises it, their script loads as if it were yours.

Example: Permissive ‘script-src’
HTML
Content-Security-Policy: script-src 'self' https://untrusted-site.com

If someone takes over https://untrusted-site.com, they can serve malicious JavaScript straight into your app. Only list sources you’d stake the page on.

3. JSONP Injection

JSONP (JSON with Padding) was an old cross-domain data trick: load a URL as a script and let it call your callback. The catch is that the callback name usually gets reflected straight into the response as executable code. If your CSP allows scripts from a domain that hosts a JSONP endpoint, an attacker can smuggle their own JavaScript through it, no matter how careful the rest of your policy is.

Example of JSONP Bypass
JS
/**
 * JSONP abuse example where attackers inject malicious payloads through external scripts.
 * @example
 * <script src="https://untrusted-site.com/callback?json_callback=alert('XSS')"></script>
 */

The attacker sets the callback to their payload, the endpoint echoes it back as a script, and the browser runs it inside your allowlist. JSONP is deprecated for exactly this reason. Reach for CORS or fetch() with application/json instead.

Real-World Examples of CSP Bypass

This isn’t theoretical. In 2016, Google researchers measured CSP across the web in a paper titled “CSP Is Dead, Long Live CSP!” and the numbers were bleak.

They found that 94.68% of policies trying to restrict script execution could be bypassed, and 99.34% of hosts running CSP had policies that offered no real protection against XSS. The main culprit was the host allowlist. Fourteen of the fifteen most-whitelisted script domains hosted a JSONP-style endpoint an attacker could abuse, so roughly three-quarters of policies were bypassable through a domain the site had deliberately trusted.

The lesson isn’t “one big site got popped.” It’s that allowlist-based CSP fails quietly and at scale. If your policy trusts a popular CDN, odds are good that CDN hosts something that will happily run an attacker’s code for you.

CSP Best Practices

Want a CSP that holds up? A few rules do most of the work.

1. Avoid Using ‘unsafe-inline’ and ‘unsafe-eval’

Keep 'unsafe-inline' and 'unsafe-eval' out unless you truly have no choice. The first reopens inline injection; the second lets strings become code through eval(), Function(), and string-argument setTimeout(). Either one guts the protection you were trying to add.

2. Limit External Script Sources

Tighten script-src and never reach for '*' or random third parties. A short allowlist beats a broad one.

Example of a Strong script-src Policy
HTML
Content-Security-Policy: script-src 'self' https://trusted-cdn.com

Only your origin and that one CDN can run scripts here, which is far better than a wildcard. Be honest about the ceiling, though. As the Google research showed, an allowlisted domain can still be turned against you if it hosts a JSONP endpoint or an open redirect. An allowlist raises the bar; it doesn’t lock the door.

3. Use Nonce-Based CSP

Nonces are the real upgrade. You generate a fresh cryptographic token for every request and attach it to your legitimate scripts, and the browser runs only the scripts carrying that exact value. Because the nonce changes on each request, an attacker injecting a script has no valid token to include.

Example: Nonce-Based CSP
HTML
Content-Security-Policy: script-src 'self' 'nonce-abcdef'

One caveat that matters: the nonce has to be random and unique on every response. A static or guessable nonce is no better than 'unsafe-inline'.

A nonce on its own still leaves the host allowlist in play, and you saw where that ends. The current guidance from Google and web.dev is to pair the nonce with 'strict-dynamic', which extends trust from your nonced script to anything it loads and tells the browser to ignore the host allowlist entirely.

Example: Strict CSP With a Nonce and ‘strict-dynamic’
HTML
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none'

Generate a new value for {RANDOM} on every request. This is the strict pattern the CSP3 spec was built around, and it’s the one worth aiming for.

4. Disable JSONP

Drop JSONP wherever it lingers. It’s one of the most reliable CSP bypasses around, and it has modern replacements. Use CORS or fetch() with secure headers and move on.

Defending Against CSP Bypasses

A few habits keep a policy honest over time:

  • Audit regularly: Re-read your policy on a schedule and hunt for directives that have drifted too permissive.
  • Turn on reporting: Configure CSP to log violations. That’s your early warning that someone is probing the page.
  • Disable what you don’t use: If your app never loads plugins or media, lock down object-src and media-src so they can’t become an entry point.
  • Bring the team along: Most CSP holes are misconfigurations. Make sure whoever touches the header knows how these bypasses work.
Conclusion

CSP is a genuinely useful control, but it protects you only as far as its configuration is tight. A loose policy is theater: it looks like security in the network tab and stops nothing. Cut 'unsafe-inline' and 'unsafe-eval', lean on per-request nonces with 'strict-dynamic', and stop trusting host allowlists to do a job they can’t.

Then keep watching. Audit the header, read your violation reports, and revisit the policy as your app and the threats around it change. Get CSP right and it quietly does its job. Get it wrong and it just tells you a comforting story about safety that isn’t true.

Leave a Comment

Your email address will not be published. Required fields are marked *


Scroll to Top