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 &lt;script&gt;, 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: &lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;
Expected Output:
Input:  <script>alert('XSS')</script>
Escaped: &lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;

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
  1. Alice is logged into bank.com with a session cookie
  2. Alice visits evil.com which contains: <img src="https://bank.com/transfer?to=attacker&amount=5000">
  3. Browser automatically sends Alice's bank.com cookie with the request
  4. 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")
HeaderProtects AgainstValue
Content-Security-PolicyXSS, data injectiondefault-src 'self'
Strict-Transport-SecuritySSL stripping, MITMmax-age=31536000; includeSubDomains
X-Frame-OptionsClickjackingDENY
X-Content-Type-OptionsMIME sniffingnosniff
Referrer-PolicyURL leakagestrict-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 hvac client
  • AWS Secrets Manager, boto3 integration
  • Key rotation strategies, zero-downtime rotation patterns