Cors Wildcard in Django with Bearer Tokens
Cors Wildcard in Django with Bearer Tokens — how this specific combination creates or exposes the vulnerability
When a Django API uses a wildcard CORS_ALLOW_ALL_ORIGINS (or Access-Control-Allow-Origin: *) while also protecting endpoints with Bearer Token authentication, the combination can unintentionally expose a cross-origin credential leak. The CORS wildcard permits any origin to make requests to your server, and if credentials are allowed, browsers will include cookies as well as HTTP authentication headers such as the Authorization header containing the Bearer token. Because the wildcard permits any domain, a malicious site can issue cross-origin requests that include the user’s Authorization header and read the response if the server does not also restrict origins for credentialed requests.
In concrete terms, a browser will send the Authorization header for a cross-origin request if withCredentials is true (or credentials: 'include' in fetch) and the server responds with an Access-Control-Allow-Credentials: true header. With a wildcard origin, the browser treats the response as readable by the calling page, enabling a cross-site script to harvest the Bearer token from the response or to perform actions on behalf of the authenticated user. This violates the same-origin policy’s isolation and effectively turns the Bearer token into a cross-origin credential.
For Django, common patterns that create this condition include:
- Setting
CORS_ALLOW_ALL_ORIGINS = Truewhile also configuringCORS_ALLOW_CREDENTIALS = True. - Using
CORSMiddlewarewithAccess-Control-Allow-Origin: *in responses while the client sendsAuthorization: Bearer <token>and the server validates the token but does not scope access by origin.
An attacker-controlled page can then perform a cross-origin fetch to your API endpoint, and because the server permits any origin and allows credentials, the browser includes the Bearer token. Even if the endpoint returns data intended only for the authenticated user, the attacker can read it. This situation is especially dangerous when the Bearer token is used for authentication and the API does not validate the Origin header or enforce strict referrer checks, because the token is effectively shared cross-origin.
The interaction between CORS and Bearer tokens is nuanced by the fact that the CORS standard treats Authorization headers as "forbidden headers" for JavaScript to set, but they can be sent by the browser automatically when credentials are included. Therefore, simply requiring Bearer tokens does not prevent a browser from including them cross-origin if the server signals that it is safe to do so via permissive CORS settings.
Bearer Tokens-Specific Remediation in Django — concrete code fixes
To secure a Django API that uses Bearer tokens while also supporting cross-origin requests, you must align CORS configuration with strict origin allowlisting and careful handling of credentials. Begin by disabling the wildcard for credentialed requests and explicitly listing trusted origins. This ensures that browsers only send the Authorization header to known origins, preventing leakage to malicious sites.
Example secure Django settings using the django-cors-headers package:
import os
# settings.py
CORS_ALLOWED_ORIGINS = [
"https://app.yourdomain.com",
"https://admin.yourdomain.com",
]
CORS_ALLOW_CREDENTIALS = True
# Ensure that your authentication does not rely on wildcard origins
CSRF_COOKIE_SAMESITE = 'Strict'
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
In this configuration, only the listed origins can make authenticated requests that include cookies or Authorization headers. The server will respond with Access-Control-Allow-Origin set to the requesting origin (not *) and include Access-Control-Allow-Credentials: true, which is required for the browser to expose the response to frontend JavaScript.
When validating Bearer tokens, ensure that token verification occurs after CORS and authentication middleware so that the origin check is applied to authenticated requests. Here is an example of a view that explicitly checks both the Authorization header and the Origin header for additional safety:
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.utils.decorators import method_decorator
from corsheaders.middleware import CORSMiddleware
@method_decorator(require_http_methods(["GET"]), name='dispatch')
class SecureApiView(View):
def dispatch(self, request, *args, **middleware):
auth = request.headers.get('Authorization', '')
if not auth.startswith('Bearer '):
return JsonResponse({'error': 'Unauthorized'}, status=401)
token = auth.split(' ')[1]
if not self.is_valid_token(token):
return JsonResponse({'error': 'Invalid token'}, status=401)
# Optionally validate request origin matches an allowed origin
origin = request.headers.get('Origin', '')
allowed_origins = os.getenv('ALLOWED_CORS_ORIGINS', '').split(',')
if origin not in allowed_origins:
return JsonResponse({'error': 'Forbidden'}, status=403)
return super().dispatch(request, *args, **kwargs)
def is_valid_token(self, token):
# Replace with your token validation logic, e.g., JWT decode
return token == 'expected-secure-token'
For SPAs, ensure that the frontend sets credentials: 'include' intentionally and that the backend does not echo the Origin header blindly. Combine this with short-lived Bearer tokens and refresh token rotation to reduce the impact of token leakage. You can also use the Vary: Origin header to prevent shared caches from serving a credentialed response to an unauthorized origin.
If you prefer to use the Django REST Framework, the same principles apply with configured permission classes and CORS settings:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
}
In summary, the key remediation steps are: replace wildcard CORS with a strict allowlist, require Access-Control-Allow-Credentials: true only for those origins, validate Bearer tokens on every request, and optionally add origin validation within your views. This prevents the Bearer token from being treated as a cross-origin credential while still allowing legitimate browser-based clients to authenticate.
Related CWEs: dataExposure
| CWE ID | Name | Severity |
|---|---|---|
| CWE-200 | Exposure of Sensitive Information | HIGH |
| CWE-209 | Error Information Disclosure | MEDIUM |
| CWE-213 | Exposure of Sensitive Information Due to Incompatible Policies | HIGH |
| CWE-215 | Insertion of Sensitive Information Into Debugging Code | MEDIUM |
| CWE-312 | Cleartext Storage of Sensitive Information | HIGH |
| CWE-359 | Exposure of Private Personal Information (PII) | HIGH |
| CWE-522 | Insufficiently Protected Credentials | CRITICAL |
| CWE-532 | Insertion of Sensitive Information into Log File | MEDIUM |
| CWE-538 | Insertion of Sensitive Information into Externally-Accessible File | HIGH |
| CWE-540 | Inclusion of Sensitive Information in Source Code | HIGH |