Django · Report #4062511
`Vary` cache-key conflation of absent and empty request fields
How it was found
UpdateCacheMiddleware / CacheMiddleware generates the same page-cache key when a request header nominated by Vary is absent and when it is present with an empty field value. This can cause a response selected for one request to be returned for the other.
RFC 9111 §4.1 says that an absent nominated request field can match only another request in which that field is also absent. HTTP field-value grammar permits an empty field value.
steps to reproduce
Use the included local-only PoC:
PYTHONPATH=/path/to/django-6.1.1 python poc_django_vary_missing_empty.py
It sets up a view where presence of X-Scope selects a PRIVATE response and the response declares Vary: X-Scope.
GET /accountwithX-Scope:storesPRIVATE.GET /accountwith noX-Scopefield returnsPRIVATEfrom cache.- The view invocation counter is
1, proving the second response was a cache hit.
Observed output:
first=PRIVATE second=PRIVATE view_calls=1
VULNERABLE: missing and empty varied headers were treated as one cache key
I also verified Django’s ASGI request adapter preserves the present-but-empty field as META["HTTP_X_SCOPE"] == ""; the collapse happens in the cache-key generator.
Impact
An application that correctly uses Vary to separate representations based on a header’s presence can have a representation cached for a request with an empty header served to a request without that header, or vice versa. If either representation contains data intended only for the other request state, this is private-response disclosure through Django’s origin cache.
The attacker capability is only to send an empty request header and to cause/request cache population; no secret-key knowledge, raw SQL, or special deployment component is needed.