1eac5d3bcc
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.
4.8 KiB
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 styleattribute blocked without'unsafe-inline'instyle-src- PDF plugins may need
object-src - Browser extensions can trigger CSP violations (ignore in reports)
frame-ancestorsdoesn't work in<meta>tag — must be HTTP header