Files
Rodin 1eac5d3bcc Add CSP, file upload, open redirect, clickjacking patterns
Complete security patterns collection (23 total):
- csp.md: nonces, hashes, strict-dynamic, reporting
- file-upload.md: content validation, path traversal, malware scanning
- open-redirect.md: URL validation, OAuth redirect URI, bypass techniques
- clickjacking.md: X-Frame-Options, frame-ancestors CSP

Comprehensive coverage for web application security review.
2026-05-10 23:24:52 -07:00

4.8 KiB

Content Security Policy (CSP)

Rule

Define strict CSP to prevent XSS. Start restrictive, loosen only as needed. Never use unsafe-inline for scripts.

Source: MDN Content Security Policy

CSP Directives

Directive Controls
default-src Fallback for all resource types
script-src JavaScript sources
style-src CSS sources
img-src Image sources
connect-src XHR, fetch, WebSocket
frame-src iframe sources
frame-ancestors Who can embed this page
form-action Form submission targets
base-uri <base> tag restrictions

Correct Pattern

# Strict CSP with nonces (recommended)
import secrets

def generate_csp_nonce() -> str:
    return secrets.token_urlsafe(16)

def get_csp_header(nonce: str) -> str:
    """Generate strict CSP header."""
    return "; ".join([
        "default-src 'self'",
        f"script-src 'nonce-{nonce}' 'strict-dynamic'",
        "style-src 'self' 'nonce-{nonce}'",
        "img-src 'self' data: https:",
        "font-src 'self'",
        "connect-src 'self' https://api.example.com",
        "frame-ancestors 'none'",
        "form-action 'self'",
        "base-uri 'self'",
        "upgrade-insecure-requests",
    ])

@app.after_request
def add_security_headers(response):
    nonce = generate_csp_nonce()
    g.csp_nonce = nonce  # Make available to templates
    response.headers["Content-Security-Policy"] = get_csp_header(nonce)
    return response

# In template:
# <script nonce="{{ g.csp_nonce }}">...</script>

Incorrect Pattern

# Wrong: unsafe-inline allows XSS
csp = "script-src 'self' 'unsafe-inline'"

# Wrong: unsafe-eval allows eval()
csp = "script-src 'self' 'unsafe-eval'"

# Wrong: wildcard allows any source
csp = "script-src *"

# Wrong: no CSP at all
# (missing header)

# Wrong: report-only without enforcement
# Use for testing, but deploy with enforcement
response.headers["Content-Security-Policy-Report-Only"] = csp
# ^ Only reports, doesn't block!

# Wrong: data: in script-src
csp = "script-src 'self' data:"
# Attacker can inject: <script src="data:text/javascript,alert(1)">

Hash-Based CSP (Alternative to Nonces)

import hashlib
import base64

def script_hash(script_content: str) -> str:
    """Generate CSP hash for inline script."""
    digest = hashlib.sha256(script_content.encode()).digest()
    return f"'sha256-{base64.b64encode(digest).decode()}'"

# For static inline scripts that don't change:
INLINE_SCRIPT = "console.log('hello');"
SCRIPT_HASH = script_hash(INLINE_SCRIPT)

csp = f"script-src 'self' {SCRIPT_HASH}"

CSP for Single Page Apps

# SPAs often need looser CSP for dynamic content
def spa_csp(nonce: str) -> str:
    return "; ".join([
        "default-src 'self'",
        # strict-dynamic allows scripts loaded by nonced scripts
        f"script-src 'nonce-{nonce}' 'strict-dynamic'",
        # SPAs often need blob: for web workers
        "worker-src 'self' blob:",
        # For inline styles from JS frameworks
        f"style-src 'self' 'nonce-{nonce}'",
        # API calls
        "connect-src 'self' https://api.example.com wss://ws.example.com",
        "frame-ancestors 'none'",
        "base-uri 'self'",
    ])

CSP Reporting

def csp_with_reporting(nonce: str) -> str:
    """CSP with violation reporting."""
    policy = get_csp_header(nonce)
    # Add reporting endpoint
    policy += "; report-uri /csp-report"
    # Or use newer report-to directive
    policy += "; report-to csp-endpoint"
    return policy

@app.route("/csp-report", methods=["POST"])
def csp_report():
    """Receive CSP violation reports."""
    report = request.get_json(force=True)
    log.warning("CSP violation", extra={
        "blocked_uri": report.get("blocked-uri"),
        "violated_directive": report.get("violated-directive"),
        "document_uri": report.get("document-uri"),
    })
    return "", 204

Gradual Rollout

# Step 1: Report-only to find issues
response.headers["Content-Security-Policy-Report-Only"] = strict_csp

# Step 2: After fixing violations, enforce
response.headers["Content-Security-Policy"] = strict_csp

# Step 3: Keep report-only for new restrictions
response.headers["Content-Security-Policy"] = current_csp
response.headers["Content-Security-Policy-Report-Only"] = stricter_csp

Edge Cases

  • Third-party scripts (analytics, widgets) need explicit sources
  • Inline event handlers (onclick) blocked by default — use addEventListener
  • style attribute blocked without 'unsafe-inline' in style-src
  • PDF plugins may need object-src
  • Browser extensions can trigger CSP violations (ignore in reports)
  • frame-ancestors doesn't work in <meta> tag — must be HTTP header