Prototype Pollution in Django with Bearer Tokens
Prototype Pollution in Django with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Prototype pollution in Django involving Bearer tokens typically arises when token handling logic merges user-controlled data into token dictionaries or when token metadata is used to drive dynamic behavior. A Bearer token is often parsed from the Authorization header and deserialized (e.g., via PyJWT), yielding a Python dict. If application code subsequently updates this dict with user input—such as merging request.query_params or request.data into the token claims—or if the token payload is used to construct objects that are later modified in-place, an attacker can inject properties that propagate to other objects via shared references.
Consider a Django view that decodes a Bearer token and then merges token claims with request data to build a context object:
import jwt
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
@require_http_methods(["POST"])
def update_profile(request):
auth = request.headers.get("Authorization")
if not auth or not auth.startswith("Bearer "):
return JsonResponse({"error": "Unauthorized"}, status=401)
token = auth.split(" ")[1]
try:
payload = jwt.decode(token, options={"verify_signature": False})
except Exception:
return JsonResponse({"error": "Invalid token"}, status=401)
# Risk: merging user input into the decoded payload dict
payload.update(request.POST.dict())
# If payload is used later to construct objects or passed to downstream logic,
# injected properties like __proto__ can pollute shared prototypes.
return JsonResponse({"profile": payload})
If an attacker sends a POST field named __proto__ or constructor, and the merged payload is used to instantiate models or passed to functions that iterate over keys, they can influence object behavior across requests that share the same class or dictionary structure. This is prototype pollution via Bearer token handling: the token’s dictionary becomes a carrier for malicious properties that persist beyond the request lifecycle when cached or reused.
Another scenario involves token-based object hydration where a Django model manager uses decoded token claims to filter or instantiate records. If claims include mutable objects or arrays and are merged with user input, an attacker can escalate privileges (BOLA/IDOR) or bypass authorization checks by injecting properties that affect permission logic. For example, injecting a role key into the token payload and then merging it into a permissions dict can lead to unauthorized access if the polluted dict is shared across users or sessions.
Middleware that inspects Bearer tokens and attaches claims to request.user or request.session also risks pollution when token claims are shallow-copied. Django’s request objects are long-lived within a thread in some WSGI configurations; if the claims dict is mutated later, the pollution can affect subsequent requests. This intersection of Bearer token parsing and mutable state is where prototype pollution becomes exploitable.
Additionally, OpenAPI/Swagger spec analysis can surface endpoints that accept both Authorization headers and body parameters that map into token-like structures. If the spec defines a security scheme using Bearer tokens but the implementation merges body data into token-derived objects, the scan can flag this as an insecure consumption pattern that may enable prototype pollution paths.
Bearer Tokens-Specific Remediation in Django — concrete code fixes
Remediation focuses on preventing user input from mutating token-derived dictionaries and ensuring copies are used where necessary. Always decode the Bearer token into a new dictionary and avoid in-place updates with request data. Prefer immutable patterns and validate claims before use.
1. Avoid mutating the decoded payload
Instead of updating the payload dict with user data, create a separate structure:
import jwt
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
@require_http_methods(["POST"])
def update_profile_safe(request):
auth = request.headers.get("Authorization")
if not auth or not auth.startswith("Bearer "):
return JsonResponse({"error": "Unauthorized"}, status=401)
token = auth.split(" ")[1]
try:
payload = jwt.decode(token, options={"verify_signature": False})
except Exception:
return JsonResponse({"error": "Invalid token"}, status=401)
# Safe: keep token claims separate from user input
user_data = request.POST.dict()
# Validate and map only expected fields
safe_data = {k: user_data[k] for k in user_data if k in ("email", "name")}
# Do NOT merge into payload; use distinct objects
return JsonResponse({"profile": safe_data, "scopes": payload.get("scopes", [])})
2. Deep copy when combining token claims and request data
If you must combine sources, use a deep copy to break shared references:
import copy
import jwt
from django.http import JsonResponse
auth = request.headers.get("Authorization")
token = auth.split(" ")[1]
payload = jwt.decode(token, options={"verify_signature": False})
# Create a deep copy to avoid polluting the original token dict
combined = copy.deepcopy(payload)
combined.update(request.POST.dict())
# Use combined for further logic; original payload remains unchanged
3. Validate and restrict token claims
Define an allowlist for claims and reject tokens with unexpected keys that could be used for pollution:
ALLOWED_CLAIMS = {"sub", "email", "scopes", "exp", "iat"}
payload = jwt.decode(token, options={"verify_signature": False})
if not ALLOWED_CLAIMS.issuperset(payload.keys()):
return JsonResponse({"error": "Invalid claims"}, status=400)
4. Use token introspection for sensitive operations
For operations that require high integrity, avoid local decoding and use a backend introspection service that returns a verified, minimal set of attributes:
# Example using a verified introspection call (pseudo-code)
claims = introspect_token(token, audience="my-api")
if claims.get("scope").contains("profile:write"):
# proceed with verified claims only
...
5. Secure Bearer token parsing patterns
Always validate the token format and avoid falling back to unsafe parsing:
from django.http import HttpResponseBadRequest
def get_bearer_token(request):
auth = request.headers.get("Authorization")
if not auth:
return None
parts = auth.split()
if len(parts) != 2 or parts[0].lower() != "bearer":
return HttpResponseBadRequest("Malformed Authorization header")
return parts[1]
These patterns reduce the risk that Bearer token handling becomes a vector for prototype pollution by isolating token state from mutable user input and enforcing strict claim validation.