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.
167 lines
4.8 KiB
Markdown
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
|