# CORS Misconfiguration ## Rule Never reflect Origin blindly. Allowlist specific origins. Don't use credentials with wildcards. **Source:** [OWASP CORS Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) ## 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 ```python 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/", methods=["OPTIONS"]) def preflight(path): response = make_response() return add_cors_headers(response) ``` ## Incorrect Pattern ```python # 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 ```python 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 ```python # Scenario 1: Data theft via reflected origin # # Vulnerable server reflects any Origin with credentials # # Attacker's evil.com: # # Scenario 2: CSRF via CORS # # If CORS allows credentials from evil.com, # evil.com can make authenticated state-changing requests ``` ## Preflight Caching ```python @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