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.
This commit is contained in:
Rodin
2026-05-10 23:24:52 -07:00
parent 17c535bc61
commit 1eac5d3bcc
5 changed files with 768 additions and 16 deletions
+174
View File
@@ -0,0 +1,174 @@
# Clickjacking
## Rule
Set X-Frame-Options or frame-ancestors CSP. Prevent your site from being embedded in attacker frames.
**Source:** [OWASP Clickjacking Defense Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html)
## How Clickjacking Works
1. Attacker creates page with invisible iframe containing your site
2. Attacker overlays convincing UI elements
3. User thinks they're clicking attacker's button
4. Actually clicking your site's button (delete, transfer, etc.)
```html
<!-- Attacker's page -->
<style>
iframe {
opacity: 0;
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 2;
}
.fake-button {
position: absolute;
top: 200px; left: 300px; /* Aligned with real button */
z-index: 1;
}
</style>
<div class="fake-button">Click to win a prize!</div>
<iframe src="https://bank.com/transfer?to=attacker&amount=10000"></iframe>
```
## Correct Pattern
```python
# Option 1: X-Frame-Options header (legacy, still works)
@app.after_request
def add_frame_options(response):
response.headers["X-Frame-Options"] = "DENY"
# Or "SAMEORIGIN" to allow same-origin framing
return response
# Option 2: CSP frame-ancestors (modern, more flexible)
@app.after_request
def add_csp(response):
response.headers["Content-Security-Policy"] = "frame-ancestors 'none'"
# Or "frame-ancestors 'self'" for same-origin
# Or "frame-ancestors 'self' https://trusted.com" for specific sites
return response
# Option 3: Both (for browser compatibility)
@app.after_request
def add_framing_protection(response):
response.headers["X-Frame-Options"] = "DENY"
response.headers["Content-Security-Policy"] = "frame-ancestors 'none'"
return response
```
## Incorrect Pattern
```python
# Wrong: no framing protection at all
# (missing headers)
# Wrong: JavaScript frame-busting only
# Can be bypassed with sandbox attribute
"""
<script>
if (top !== self) {
top.location = self.location;
}
</script>
"""
# Bypassed by: <iframe src="bank.com" sandbox="allow-forms"></iframe>
# Wrong: ALLOWALL (defeats the purpose)
response.headers["X-Frame-Options"] = "ALLOWALL"
# Wrong: checking via JavaScript after load
# Attacker can disable JS or race the check
```
## When Framing IS Needed
```python
# If you need to allow specific partners to embed:
ALLOWED_FRAME_ANCESTORS = ["https://partner1.com", "https://partner2.com"]
@app.after_request
def conditional_framing(response):
# Pages that should never be framed
if request.path.startswith("/admin") or request.path.startswith("/settings"):
response.headers["Content-Security-Policy"] = "frame-ancestors 'none'"
# Embeddable widgets
elif request.path.startswith("/embed/"):
ancestors = " ".join(ALLOWED_FRAME_ANCESTORS)
response.headers["Content-Security-Policy"] = f"frame-ancestors {ancestors}"
# Default: same-origin only
else:
response.headers["Content-Security-Policy"] = "frame-ancestors 'self'"
return response
```
## Double-Framing Defense
```python
# Attacker might try: evil.com -> trusted.com -> your-site.com
# frame-ancestors 'self' https://trusted.com would allow this!
# Defense: Only allow direct framing
@app.after_request
def strict_framing(response):
# Check if request came from an allowed embedder
# Note: Referer can be spoofed, this is defense-in-depth
referer = request.headers.get("Referer", "")
if is_embed_request(request):
if not any(referer.startswith(a) for a in ALLOWED_FRAME_ANCESTORS):
response.headers["Content-Security-Policy"] = "frame-ancestors 'none'"
return response
# Also set on response so browsers enforce
ancestors = " ".join(ALLOWED_FRAME_ANCESTORS)
response.headers["Content-Security-Policy"] = f"frame-ancestors {ancestors}"
return response
```
## Sensitive Actions
```python
# Clickjacking is most dangerous for state-changing actions
# Add extra protection for these:
def require_confirmation(f):
"""Require explicit confirmation for sensitive actions."""
@wraps(f)
def decorated(*args, **kwargs):
# Require POST with CSRF token
if request.method != "POST":
abort(405)
# Verify CSRF
if not validate_csrf_token(request.form.get("csrf_token")):
abort(403)
# Optional: require re-authentication for very sensitive actions
# Optional: add CAPTCHA
return f(*args, **kwargs)
return decorated
@app.route("/account/delete", methods=["POST"])
@require_confirmation
def delete_account():
# Clickjacking can't easily bypass POST + CSRF
pass
```
## Edge Cases
- Mobile apps using WebViews may legitimately embed your site
- PDF embedding (`<embed>`, `<object>`) not covered by frame-ancestors
- Legacy IE doesn't support CSP frame-ancestors, needs X-Frame-Options
- frame-ancestors must be in HTTP header, not `<meta>` tag
- Cursorjacking: manipulating cursor position (similar attack)
- Likejacking: clicking social media Like buttons