Summary
Django REST framework: TokenProxy.delete() reports success and revokes nothing
Deleting an authentication token through TokenProxy silently deletes nothing. The call raises no
exception and reports success, the database row survives, and the token keeps authenticating. Any
application, admin integration or operational script that revokes credentials by calling .delete()
on a TokenProxy instance believes it has revoked a token that is still live.
Details
TokenProxy overrides pk to return the user id (rest_framework/authtoken/models.py:49-63):
@property
def pk(self):
return self.user_id
The concrete Token model keys on key, not on an integer id. Model.delete() builds its delete
query from pk, so it emits:
DELETE FROM "authtoken_token" WHERE "authtoken_token"."key" IN ('1')
That matches a user id against the 40-character key column. Nothing matches, nothing raises, and the
call returns (0, {'authtoken.TokenProxy': 0}).
DRF already ships two workarounds for this same identity mismatch instead of a fix. Inrest_framework/authtoken/admin.py, TokenAdmin.delete_model re-fetches the concrete row:
def delete_model(self, request, obj):
Token.objects.get(key=obj.key).delete() # admin.py:30
and TokenAdmin sets actions = None with the comment "Actions not compatible with mapped IDs"
(admin.py:50-53). The shipped admin path is therefore safe, and every other caller ofTokenProxy.delete() is not. Nothing in the API signals the difference.
TokenProxy was added by e2bd3b6a (PR #7341, "Adjusted token admin to map to user ID"), first
released in 3.12.0.
A fix would drop the pk override, or give TokenProxy a delete() that operates on the concrete
row, or raise instead of silently reporting zero deletions.
PoC
Save as poc_tokenproxy_revocation.py and run it.
#!/usr/bin/env python
"""
django-rest-framework: TokenProxy.delete() reports success and revokes nothing.
baseline token exists -> 200
TRIGGER TokenProxy.objects.get(...).delete() -> no exception, row survives, still 200
CONTROL Token.objects.get(key=...).delete() -> row gone, 401
The control is what makes the trigger meaningful: it proves the row was
deletable and the credential check live, so the 200 after the proxy delete is a
failed revocation and not a broken harness.
Requires: django, djangorestframework importable. No arguments.
python poc_tokenproxy_revocation.py
Exit status 0 means the revocation failure reproduced.
"""
import sys
import django
from django.conf import settings
settings.configure(
DEBUG=False,
SECRET_KEY="poc",
ALLOWED_HOSTS=["*"],
ROOT_URLCONF=__name__,
INSTALLED_APPS=[
"django.contrib.contenttypes",
"django.contrib.auth",
"rest_framework",
"rest_framework.authtoken",
],
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
DEFAULT_AUTO_FIELD="django.db.models.AutoField",
USE_TZ=True,
)
django.setup()
from django.contrib.auth import get_user_model # noqa: E402
from django.core.management import call_command # noqa: E402
from django.db import connection # noqa: E402
from django.test import Client # noqa: E402
from django.test.utils import CaptureQueriesContext # noqa: E402
from django.urls import path # noqa: E402
from rest_framework.authentication import TokenAuthentication # noqa: E402
from rest_framework.authtoken.models import Token, TokenProxy # noqa: E402
from rest_framework.permissions import IsAuthenticated # noqa: E402
from rest_framework.response import Response # noqa: E402
from rest_framework.views import APIView # noqa: E402
class Whoami(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request):
return Response({"user": request.user.get_username()})
urlpatterns = [path("", Whoami.as_view())]
def probe(client, key):
response = client.get("/", headers={"authorization": "Token %s" % key})
return response.status_code
def main():
call_command("migrate", run_syncdb=True, verbosity=0)
user = get_user_model().objects.create_user(username="revoke-user", password="pw")
key = Token.objects.create(user=user).key
client = Client()
print("TokenProxy._meta.proxy =", TokenProxy._meta.proxy)
print("Token._meta.pk.attname =", Token._meta.pk.attname)
print("user.pk =", user.pk)
print("token key =", key)
print("\n--- baseline")
print("rows =", Token.objects.count())
baseline = probe(client, key)
print("GET / with token ->", baseline)
print("\n--- TRIGGER: delete through the proxy")
proxy = TokenProxy.objects.get(user=user)
print("proxy.pk =", proxy.pk, "(TokenProxy.pk property)")
raised = None
with CaptureQueriesContext(connection) as ctx:
try:
returned = proxy.delete()
except Exception as exc: # noqa: BLE001
raised, returned = exc, None
for q in ctx.captured_queries:
if "DELETE" in q["sql"]:
print(" SQL:", q["sql"])
print("raised =", repr(raised))
print("returned =", returned)
print("rows =", Token.objects.count())
after_proxy = probe(client, key)
print("GET / with token ->", after_proxy)
print("\n--- CONTROL: delete the same row through the concrete model")
with CaptureQueriesContext(connection) as ctx2:
returned2 = Token.objects.get(key=key).delete()
for q in ctx2.captured_queries:
if "DELETE" in q["sql"]:
print(" SQL:", q["sql"])
print("returned =", returned2)
print("rows =", Token.objects.count())
after_real = probe(client, key)
print("GET / with token ->", after_real)
ok = baseline == 200 and raised is None and after_proxy == 200 and after_real == 401
print(
"\nrevocation failure reproduced:", ok,
"(baseline 200, still 200 after proxy delete, 401 after real delete)",
)
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())
Observed output:
TokenProxy._meta.proxy = True
Token._meta.pk.attname = key
user.pk = 1
token key = 6a644878f252175d39205804c5a6faf16df67c10
--- baseline
rows = 1
GET / with token -> 200
--- TRIGGER: delete through the proxy
proxy.pk = 1 (TokenProxy.pk property)
SQL: DELETE FROM "authtoken_token" WHERE "authtoken_token"."key" IN ('1')
raised = None
returned = (0, {'authtoken.TokenProxy': 0})
rows = 1
GET / with token -> 200
--- CONTROL: delete the same row through the concrete model
SQL: DELETE FROM "authtoken_token" WHERE "authtoken_token"."key" IN ('6a644878...')
returned = (1, {'authtoken.Token': 1})
rows = 0
GET / with token -> 401
revocation failure reproduced: True
The control is what makes the trigger meaningful: it proves the row was deletable and the credential
check live, so the 200 after the proxy delete is a failed revocation and not a broken harness.
Reproduced identically on Django 6.1 and 5.2.17 LTS, so this is not a recent Django regression.
Impact
Failed credential revocation. An operator who revokes a token through a TokenProxy instance, and
receives no error, leaves a live credential in circulation indefinitely, because DRF auth tokens do
not expire on their own. Whoever holds that token benefits: a departed employee, a rotated
integration, a device believed to be deprovisioned, or an attacker whose access was thought to have
been cut during incident response.
No untrusted input reaches this code path, which is why the vector is scored AC:H. Exploitation
depends on the deployment revoking through the proxy, which is a natural thing to do givenTokenProxy is the model registered in the admin and the one most code will reach for. Any
deployment on 3.12.0 or later that revokes tokens outside DRF's own TokenAdmin.delete_model should
audit whether its revocations took effect.
CVE-2026-114137 has a CVSS score of 7.4 (High). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (3.18.2); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-114137? CVE-2026-114137 is a high-severity security vulnerability in djangorestframework (pip), affecting versions >= 3.12.0, < 3.18.2. It is fixed in 3.18.2.
- How severe is CVE-2026-114137? CVE-2026-114137 has a CVSS score of 7.4 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of djangorestframework are affected by CVE-2026-114137? djangorestframework (pip) versions >= 3.12.0, < 3.18.2 is affected.
- Is there a fix for CVE-2026-114137? Yes. CVE-2026-114137 is fixed in 3.18.2. Upgrade to this version or later.
- Is CVE-2026-114137 exploitable, and should I be worried? Whether CVE-2026-114137 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-114137 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-114137? Upgrade
djangorestframeworkto 3.18.2 or later.