# 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` | `` 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:
#
```
## 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: