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

167 lines
4.8 KiB
Markdown

# 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](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
## 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
```python
# 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
```python
# 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)
```python
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
```python
# 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
```python
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
```python
# 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