Web Security
XSS, CSRF, Clickjacking, Security Headers, CORS, Cookie Security
Introduction
Web applications face a unique set of browser-based attacks beyond injection. XSS, CSRF, and clickjacking exploit the trust relationship between users and their browsers. Security headers tell browsers how to protect users, and proper cookie configuration prevents entire classes of session theft. This lesson covers each threat with Python/Flask examples and the precise defenses.
XSS, Cross-Site Scripting
Reflected XSS
Malicious script is in the URL/form input, reflected back in the response. Requires the victim to click a crafted link.
Stored XSS
Malicious script stored in the database (e.g., comment, profile name). Served to every user who views the content.
DOM-Based XSS
Malicious script executed via client-side JavaScript that reads unsanitized data from the URL or DOM.
Install: pip install flask markupsafe
# xss_demo.py, Flask XSS example
from flask import Flask, request
from markupsafe import escape
app = Flask(__name__)
# ❌ VULNERABLE, Reflects user input without escaping
@app.route("/search/vulnerable")
def search_vulnerable():
query = request.args.get("q", "")
# Attack URL: /search/vulnerable?q=<script>document.cookie='stolen='+document.cookie</script>
return f"<html><body>Search results for: {query}</body></html>"
# The script tag executes in the victim's browser!
# ✅ FIXED, Escape user input with markupsafe
@app.route("/search/safe")
def search_safe():
query = request.args.get("q", "")
safe_query = escape(query) # Converts < > & " ' to HTML entities
# <script> becomes <script>, rendered as text, not executed
return f"<html><body>Search results for: {safe_query}</body></html>"
# ✅ ALSO: Use Content Security Policy header
@app.after_request
def add_security_headers(response):
# CSP prevents inline scripts from executing
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self'; " # No inline scripts
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: https:; "
"object-src 'none'"
)
return response
# Demonstrate escaping
attack_input = "<script>alert('XSS')</script>"
safe_output = escape(attack_input)
print(f"Input: {attack_input}")
print(f"Escaped: {safe_output}")
# Output: <script>alert('XSS')</script>
Expected Output:
Input: <script>alert('XSS')</script>
Escaped: <script>alert('XSS')</script>CSRF, Cross-Site Request Forgery
CSRF tricks a logged-in user's browser into making an unauthorized request to a site where the user is authenticated. The browser automatically sends session cookies, so the server can't distinguish a real request from a forged one.
Attack Scenario
- Alice is logged into bank.com with a session cookie
- Alice visits evil.com which contains:
<img src="https://bank.com/transfer?to=attacker&amount=5000"> - Browser automatically sends Alice's bank.com cookie with the request
- Bank executes the transfer, Alice's session was valid!
Install: pip install flask flask-wtf
# csrf_protection.py, CSRF token implementation
from flask import Flask, session, request
import secrets
import hmac
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
# ── Server-side CSRF token ─────────────────────────────────────
def generate_csrf_token() -> str:
"""Generate a cryptographically secure CSRF token for the session."""
if "csrf_token" not in session:
session["csrf_token"] = secrets.token_hex(32)
return session["csrf_token"]
def validate_csrf_token(token: str) -> bool:
"""Validate CSRF token using constant-time comparison."""
expected = session.get("csrf_token", "")
return hmac.compare_digest(expected, token)
@app.route("/transfer", methods=["GET"])
def transfer_form():
token = generate_csrf_token()
return f"""
<form method="POST">
<!-- CSRF token hidden field, must match session -->
<input type="hidden" name="csrf_token" value="{token}">
<input name="amount" placeholder="Amount">
<input name="to_account" placeholder="Account">
<button>Transfer</button>
</form>
"""
@app.route("/transfer", methods=["POST"])
def transfer_submit():
# ✅ Validate CSRF token before processing
submitted_token = request.form.get("csrf_token", "")
if not validate_csrf_token(submitted_token):
return "CSRF validation failed", 403
# Process legitimate request
amount = request.form.get("amount")
return f"Transfer of ${amount} processed"
print("CSRF Protection Summary:")
print(" 1. Generate unique token per session")
print(" 2. Include token in every state-changing form")
print(" 3. Verify token on every POST/PUT/DELETE")
print(" 4. Use SameSite=Strict cookies (next section)")
print(" 5. Flask-WTF does all this automatically")
Security Headers & Cookie Security
# security_headers.py, Flask middleware for security headers
from flask import Flask, make_response
app = Flask(__name__)
@app.after_request
def apply_security_headers(response):
"""Add comprehensive security headers to every response."""
# Prevent MIME type sniffing
response.headers["X-Content-Type-Options"] = "nosniff"
# Prevent clickjacking, don't allow embedding in iframes
response.headers["X-Frame-Options"] = "DENY"
# Enable browser XSS filter (legacy, but harmless)
response.headers["X-XSS-Protection"] = "1; mode=block"
# Force HTTPS for 1 year (only on HTTPS sites)
response.headers["Strict-Transport-Security"] = (
"max-age=31536000; includeSubDomains; preload"
)
# Content Security Policy, restrict resource loading
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self'; "
"style-src 'self' https://cdn.jsdelivr.net; "
"font-src 'self' https://fonts.gstatic.com; "
"img-src 'self' data: https:; "
"connect-src 'self'; "
"frame-ancestors 'none'; "
"base-uri 'self'; "
"form-action 'self'"
)
# Referrer policy, don't leak full URLs
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
# Permissions policy, disable unused browser features
response.headers["Permissions-Policy"] = (
"camera=(), microphone=(), geolocation=(), "
"interest-cohort=()"
)
return response
# ── Secure cookie configuration ──────────────────────────────
@app.route("/login", methods=["POST"])
def login():
response = make_response({"status": "logged in"})
response.set_cookie(
"session_id",
value="abc123xyz",
httponly=True, # Not accessible via JavaScript, prevents XSS theft
secure=True, # Only sent over HTTPS
samesite="Strict", # Not sent in cross-origin requests, prevents CSRF
max_age=3600, # Expires in 1 hour
path="/",
)
return response
print("✅ Security headers add browser-enforced protections")
print("✅ HttpOnly + Secure + SameSite=Strict is the gold standard for cookies")
| Header | Protects Against | Value |
|---|---|---|
Content-Security-Policy | XSS, data injection | default-src 'self' |
Strict-Transport-Security | SSL stripping, MITM | max-age=31536000; includeSubDomains |
X-Frame-Options | Clickjacking | DENY |
X-Content-Type-Options | MIME sniffing | nosniff |
Referrer-Policy | URL leakage | strict-origin-when-cross-origin |
Key Takeaways
- Escape all output, use
markupsafe.escape()or a template engine that auto-escapes (Jinja2, React JSX) - CSP is the strongest XSS defense, a strict Content-Security-Policy blocks inline scripts even if injection occurs
- CSRF tokens protect state-changing requests, include in every POST/PUT/DELETE form; use Flask-WTF for automatic handling
- SameSite=Strict prevents CSRF via cookies, modern browsers enforce this; still use CSRF tokens for defense-in-depth
- HttpOnly prevents XSS cookie theft, session cookies should always be HttpOnly and Secure
- CORS misconfiguration enables CSRF, never use
Access-Control-Allow-Origin: *for authenticated endpoints
What's Next?
Lesson 15 tackles one of the most common production failures: secrets management — how to store, rotate, and access API keys, passwords, and certificates safely.
- python-dotenv patterns, secrets validation at startup
- HashiCorp Vault, Python
hvacclient - AWS Secrets Manager,
boto3integration - Key rotation strategies, zero-downtime rotation patterns