Engineering · Sep 5, 2026 · 7 min read

Content Security Policy bypass: when the policy permits what it should block

A CSP with unsafe-inline, a wildcard source, or a whitelisted JSONP endpoint undoes the protection the policy was built to provide - each bypass path needs a different confirming probe.

Content Security Policy is the browser-enforced barrier between a reflected or stored injection and a script-execution event. The moment a policy contains unsafe-inline, a wildcard CDN source, or a whitelisted endpoint that serves caller-controlled JSONP callbacks, that barrier collapses. The HTTP header is present, a scanner that only checks presence marks it compliant, and the injection the policy was supposed to stop executes anyway. Confirming which pattern caused the bypass - and at what severity - requires parsing the directive, finding the corresponding sink, and proving execution with a benign oracle.

01The directives that dissolve a policy

A CSP restricts which scripts may run in a browsing context by naming approved sources in script-src, or in default-src as a fallback. Four directive patterns each dissolve that restriction in a different way. 'unsafe-inline' permits every inline <script> element and every event-handler attribute - the exact injection surface that XSS attacks target. 'unsafe-eval' permits eval(), setTimeout(string), and the Function() constructor, converting a string-injection sink into a code-execution sink. A broad CDN host entry or a host-level wildcard permits any path on that host; if any of those paths serve JSONP with a caller-controlled callback name, an attacker who injects a <script src> pointing at that endpoint receives execution from a browser-trusted source. Finally, a Content-Security-Policy-Report-Only header documents violations without blocking anything - a policy that logs the attack after the fact is not a defence.

strict nonce policy inject: <script>evil()</script> ──► blocked (no matching nonce) policy with 'unsafe-inline' inject: <script>evil()</script> ──► executes: inline scripts permitted policy with allowlisted cdn.example.com inject: <script src='cdn.example.com/jsonp?cb=evil'></script> ──► executes via JSONP callback (CDN is allowlisted) Content-Security-Policy-Report-Only (no enforcement) inject: <script>evil()</script> ──► executes; violation report fires after
Header present, policy bypassed. Each path requires a different probe to confirm and a different directive to fix.

02JSONP callbacks and CDN allowlist abuse

A JSONP endpoint wraps its JSON response in a caller-supplied function name and returns it as JavaScript. If a host in the CSP allowlist serves such an endpoint - a pattern common to public APIs and CDNs with analytics, widget, or autocomplete endpoints - an attacker who can inject a <script src> attribute points it at that endpoint with an arbitrary callback name. The browser fetches a URL from an allowlisted host, receives syntactically valid JavaScript calling the attacker-controlled function, and executes it. The CSP never fires: no inline script, no unknown source, no violation logged. This path also applies to open redirects on allowlisted hosts - if the redirect destination returns JavaScript, the policy does not re-evaluate the final URL after the hop. The root cause in both cases is that script-src trusts a host, not a specific response content-type, so any script-flavored response from that host is granted execution.

csp-comparison.httphttp
# Vulnerable: unsafe-inline present, broad CDN host
Content-Security-Policy:
  default-src  'self';
  script-src   'self' 'unsafe-inline'         # XSS bypass
                https://cdn.example.com  # JSONP bypass if endpoint exists

# Strict: nonce + strict-dynamic, no host wildcards
Content-Security-Policy:
  default-src  'none';
  script-src   'nonce-{per-request}' 'strict-dynamic';
  object-src  'none';
  base-uri    'none';
Proven - HighCSP bypass via unsafe-inline - reflected XSS execution - /search
GET /search?q=<script>document.title='62615533'</script> ──► 200 OK <title>62615533</title> (script executed; oracle value in DOM title) CSP: script-src 'self' 'unsafe-inline' (policy present, bypass confirmed) baseline GET /search?q=hello ──► <title>Search</title> - differential holds across 3 repeats
Oracle: arithmetic nonce 62615533 via document.title mutation. No alert(), no external resource, no real payload - inline script execution proven in-band. Rated high: any victim loading the crafted URL executes attacker script under the application origin. Escalates to critical when the endpoint is authenticated and the script can read session data.

03Confirming bypass: parse the directive before probing

Confirming a CSP bypass follows two phases, and the first is entirely static. Read every response header for both the enforcing directive and the report-only variant, then tokenize the directive list and flag weaknesses before sending a single probe. A policy without a script-src entry falls back to default-src; a missing object-src 'none' opens plugin-injection paths even when script-src is tight; a missing base-uri 'none' lets a base-tag injection pivot the effective script origin. Phase two is dynamic and only fires when a reflected or stored output sink exists: chain the injection to the policy state. The arithmetic oracle document.title='62615533' inside an injected script element proves inline execution without triggering any real script behavior or loading any external resource. For a JSONP path, request the candidate endpoint with a callback name containing the nonce - if the response body opens with probe62615533({...}), the callback is reflected unescaped and the path is an active bypass vector. Both phases are read-only: no writes, no real domain contacted beyond the target application.

Bypass vectorPolicy signalConfirming probeSeverity
unsafe-inlinescript-src or default-src contains 'unsafe-inline'Arithmetic nonce via inline script; document.title oracleHigh (critical on authenticated endpoint)
unsafe-evalscript-src contains 'unsafe-eval'eval-reachable DOM mutation oracleHigh
JSONP via allowlisted hostCDN or API host in script-srcCallback nonce probe; response opens with probe function nameHigh
Report-Only, no enforcementCSP-Report-Only present; no blocking headerInline script executes unblocked; violation report confirms sinkHigh (policy absent from enforcement path)
Missing object-src or base-uriDirectives absent from otherwise strict policyBase-tag injection shifts script-src origin anchorMed (depends on reachable sink)

04Severity grading and remediation

Severity rests on two independent facts: the strength of the policy gap and the reachability of an injection sink. An unsafe-inline policy with no reflected or stored injection anywhere in the tested surface is rated medium - the bypass mechanism is confirmed, but no execution path was demonstrated. The same policy paired with a confirmed probe that executed the oracle is high, or critical when the endpoint is authenticated and the injected script runs in a real user session. A report-only header with no blocking counterpart is always high regardless of whether a sink was found - the policy is documentation, not defence, and any injection that fires against it executes without the browser raising an objection. Severity is never raised for policy presence alone: a strict nonce-only policy with no unsafe keywords and no broad host entries contributes nothing to the risk score even if a reflected parameter exists, because the policy actually prevents execution.

5
bypass patterns graded separately
0
real payloads or external fetches in probe
2
header variants checked (enforce + report-only)

The remediation runs in parallel on the policy and the injection sink. A nonce-based policy with strict-dynamic is the highest-assurance configuration: each page load generates a cryptographically random nonce applied server-side to every legitimate script element; an injected inline script carries no nonce and is blocked. strict-dynamic propagates trust to scripts loaded by a nonce-carrying parent, allowing most CDN host entries to be dropped from the allowlist and closing the JSONP bypass surface in the same change. object-src 'none' and base-uri 'none' are required alongside it - omitting either opens a side channel even when script-src is tight. The parallel action is always output encoding at every injection sink: CSP is a defence-in-depth layer, not a substitute for sanitizing what reaches the response.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →