CORS Security


Introduction

Cross-Origin Resource Sharing (CORS) is a browser mechanism that controls which origins can access resources on a different origin. While CORS enables legitimate cross-origin requests, misconfigurations are among the most common security vulnerabilities discovered in modern web applications.

How CORS Works

CORS works through HTTP headers that the server sends to tell the browser which origins are permitted. The browser enforces these restrictions on the client side.

Simple Requests

A simple request uses standard methods (GET, HEAD, POST) and headers. The browser adds an `Origin` header, and the server responds with `Access-Control-Allow-Origin`.




Request:


GET /api/data HTTP/1.1


Origin: https://trusted-app.com




Response:


HTTP/1.1 200 OK


Access-Control-Allow-Origin: https://trusted-app.com





Preflight Requests

For non-simple requests (custom headers, PUT, DELETE, content types other than form data), the browser sends an OPTIONS preflight request first.




Preflight:


OPTIONS /api/data HTTP/1.1


Origin: https://trusted-app.com


Access-Control-Request-Method: DELETE


Access-Control-Request-Headers: X-Custom-Header




Response:


HTTP/1.1 204 No Content


Access-Control-Allow-Origin: https://trusted-app.com


Access-Control-Allow-Methods: GET, POST, PUT, DELETE


Access-Control-Allow-Headers: X-Custom-Header


Access-Control-Max-Age: 3600





Proper Origin Validation

Never reflect the `Origin` header back unconditionally. This is the most common dangerous CORS misconfiguration.




# UNSAFE: Reflective origin (vulnerable to attack)


def cors_unsafe(request):


origin = request.headers.get('Origin')


response.headers['Access-Control-Allow-Origin'] = origin # NEVER do this


response.headers['Access-Control-Allow-Credentials'] = 'true'




# SAFE: Whitelist-based origin validation


ALLOWED_ORIGINS = {


'https://app.example.com',


'https://admin.example.com',


'https://trusted-partner.com',


}




def cors_safe(request):


origin = request.headers.get('Origin')




if origin in ALLOWED_ORIGINS:


response.headers['Access-Control-Allow-Origin'] = origin


response.headers['Vary'] = 'Origin'




if origin and is_origin_allowed(origin):


response.headers['Access-Control-Allow-Origin'] = origin


response.headers['Vary'] = 'Origin'




def is_origin_allowed(origin):


from urllib.parse import urlparse


parsed = urlparse(origin)




# Must be HTTPS


if parsed.scheme != 'https':


return False




# Exact match only, no wildcards for credentialed requests


return parsed.geturl() in ALLOWED_ORIGINS





Common Misconfigurations

1\. Wildcard with Credentials




# VULNERABLE: Wildcard origin with credentials


response.headers['Access-Control-Allow-Origin'] = '*'


response.headers['Access-Control-Allow-Credentials'] = 'true'


# Browsers will reject this combination, but attackers can still exploit


# applications that don't rely on browser enforcement (e.g., server-side CORS)





2\. Overly Permissive Origin Match




# VULNERABLE: Subdomain matching that is too permissive


def unsafe_origin_check(origin):


# Accepts evil.example.com as matching example.com


return origin.endswith('.example.com') or origin == 'example.com'


# Also vulnerable: origin contains 'example.com' would match evil-example.com





3\. Null Origin




# VULNERABLE: Allow null origin


if origin == 'null':


response.headers['Access-Control-Allow-Origin'] = 'null'


# null origin is used by file://, sandboxed iframes, and some


# automated scanners. Attackers can exploit this.





Exploit Scenarios

Internal Network API Attack

An attacker hosts a malicious site that performs cross-origin requests to internal APIs. If the internal API reflects origins, the attacker can exfiltrate sensitive data.









CORS Configuration Best Practices




from flask import Flask, request, jsonify


from functools import wraps




app = Flask(__name__)




def cors_allowed(origin):


ALLOWED = [


'https://app.example.com',


'https://dashboard.example.com',


]


return origin in ALLOWED




def cors_middleware(f):


@wraps(f)


def decorated(*args, **kwargs):


origin = request.headers.get('Origin')




if request.method == 'OPTIONS':


response = app.make_default_options_response()


else:


response = f(*args, **kwargs)




if origin and cors_allowed(origin):


response.headers['Access-Control-Allow-Origin'] = origin


response.headers['Vary'] = 'Origin'


response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'


response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'


response.headers['Access-Control-Max-Age'] = '3600'




if request.cookies:


response.headers['Access-Control-Allow-Credentials'] = 'true'




return response


return decorated





Conclusion

CORS misconfigurations remain a top web vulnerability. Never reflect origins dynamically, never use wildcard with credentials, validate origins against an allowlist, and always set the `Vary: Origin` header when using dynamic CORS. Remember that CORS is a browser-enforced policy — protect sensitive endpoints with proper authentication regardless.