CVE-2026-114137
Django REST framework: TokenProxy.delete() reports success and revokes nothing
Django REST framework is a toolkit for building Web APIs. From 3.12.0 until 3.18.2, TokenProxy.delete() in rest_framework/authtoken/models.py uses the proxy's user-id pk while the concrete Token token-string primary key identifies rows, so Django issues a deletion query that matches no row and reports zero deletions without raising an error. Applications, administrative integrations, or operational scripts that revoke credentials through a TokenProxy instance can therefore believe revocation succeeded while the token remains live and continues authenticating. DRF's own TokenAdmin.delete_model workaround is not affected, but other callers can leave access available indefinitely because DRF tokens do not expire automatically. This issue is fixed in version 3.18.2.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- High
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
7.4
Django REST framework: TokenProxy.delete() reports success and revokes nothing
Django REST framework is a toolkit for building Web APIs. From 3.12.0 until 3.18.2, TokenProxy.delete() in rest_framework/authtoken/models.py uses the proxy's user-id pk while the concrete Token token-string primary key identifies rows, so Django issues a deletion query that matches no row and reports zero deletions without raising an error. Applications, administrative integrations, or operational scripts that revoke credentials through a TokenProxy instance can therefore believe revocation succeeded while the token remains live and continues authenticating. DRF's own TokenAdmin.delete_model workaround is not affected, but other callers can leave access available indefinitely because DRF tokens do not expire automatically. This issue is fixed in version 3.18.2.
- Record
CVE-2026-114137- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-390, CWE-613
- Updated
- 2026-11-02
EUVDEUVD-2026-107547
7.4
Django REST framework is a toolkit for building Web APIs. From 3.12.0 until 3.18.2, TokenProxy.delete() in rest_framework/authtoken/models.py uses the proxy's user-id pk while the concrete Token token-string primary key identifies rows, so Django issues a deletion query that matches no row and reports zero deletions without raising an error. Applications, administrative integrations, or operational scripts that revoke credentials through a TokenProxy instance can therefore believe revocation succeeded while the token remains live and continues authenticating. DRF's own TokenAdmin.delete_model workaround is not affected, but other callers can leave access available indefinitely because DRF tokens do not expire automatically. This issue is fixed in version 3.18.2.
- Record
EUVD-2026-107547- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-xw6w-gcfp-cf8m
7.4
Django REST framework: TokenProxy.delete() reports success and revokes nothing
Summary
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. In
rest_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 of
TokenProxy.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("/",…
- Record
GHSA-xw6w-gcfp-cf8m- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-390, CWE-613
- Updated
- 2026-11-02
NVD
7.4
Django REST framework is a toolkit for building Web APIs. From 3.12.0 until 3.18.2, TokenProxy.delete() in rest_framework/authtoken/models.py uses the proxy's user-id pk while the concrete Token token-string primary key identifies rows, so Django issues a deletion query that matches no row and reports zero deletions without raising an error. Applications, administrative integrations, or operational scripts that revoke credentials through a TokenProxy instance can therefore believe revocation succeeded while the token remains live and continues authenticating. DRF's own TokenAdmin.delete_model workaround is not affected, but other callers can leave access available indefinitely because DRF tokens do not expire automatically. This issue is fixed in version 3.18.2.
- Record
CVE-2026-114137- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-390, CWE-613
- Updated
- 2026-11-02
OSV
7.4
Django REST framework: TokenProxy.delete() reports success and revokes nothing
Django REST framework is a toolkit for building Web APIs. From 3.12.0 until 3.18.2, TokenProxy.delete() in rest_framework/authtoken/models.py uses the proxy's user-id pk while the concrete Token token-string primary key identifies rows, so Django issues a deletion query that matches no row and reports zero deletions without raising an error. Applications, administrative integrations, or operational scripts that revoke credentials through a TokenProxy instance can therefore believe revocation succeeded while the token remains live and continues authenticating. DRF's own TokenAdmin.delete_model workaround is not affected, but other callers can leave access available indefinitely because DRF tokens do not expire automatically. This issue is fixed in version 3.18.2.
- Record
CVE-2026-114137- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-390, CWE-613
- Updated
- 2026-11-02
OSVDEBIAN-CVE-2026-114137
7.4
Django REST framework is a toolkit for building Web APIs. From 3.12.0 until 3.18.2, TokenProxy.delete() in rest_framework/authtoken/models.py uses the proxy's user-id pk while the concrete Token token-string primary key identifies rows, so Django issues a deletion query that matches no row and reports zero deletions without raising an error. Applications, administrative integrations, or operational scripts that revoke credentials through a TokenProxy instance can therefore believe revocation succeeded while the token remains live and continues authenticating. DRF's own TokenAdmin.delete_model workaround is not affected, but other callers can leave access available indefinitely because DRF tokens do not expire automatically. This issue is fixed in version 3.18.2.
- Record
DEBIAN-CVE-2026-114137- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Updated
- 2026-11-02
Show 4 moreShow less
OSVGHSA-xw6w-gcfp-cf8m
7.4
Django REST framework: TokenProxy.delete() reports success and revokes nothing
Summary
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. In
rest_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 of
TokenProxy.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("/",…
- Record
GHSA-xw6w-gcfp-cf8m- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-390, CWE-613
- Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-114137
7.4
Django REST framework is a toolkit for building Web APIs. From 3.12.0 until 3.18.2, TokenProxy.delete() in rest_framework/authtoken/models.py uses the proxy's user-id pk while the concrete Token token-string primary key identifies rows, so Django issues a deletion query that matches no row and reports zero deletions without raising an error. Applications, administrative integrations, or operational scripts that revoke credentials through a TokenProxy instance can therefore believe revocation succeeded while the token remains live and continues authenticating. DRF's own TokenAdmin.delete_model workaround is not affected, but other callers can leave access available indefinitely because DRF tokens do not expire automatically. This issue is fixed in version 3.18.2.
- Record
UBUNTU-CVE-2026-114137- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N - Updated
- 2026-11-02
OSVopenSUSE-SU-2026:12277-1 No score
python313-djangorestframework-3.18.2-1.1 on GA media
These are all security issues fixed in the python313-djangorestframework-3.18.2-1.1 package on the GA media of openSUSE Tumbleweed.
- Record
openSUSE-SU-2026:12277-1- Updated
- 2026-11-02
OSVROOT-APP-PYPI-CVE-2026-114137 No score
CVE-2026-114137 in djangorestframework - Patched by Root
Root has patched CVE-2026-114137 in the djangorestframework package for Root:PyPI. Multiple fixed versions available.
- Record
ROOT-APP-PYPI-CVE-2026-114137- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
djangorestframeworkPyPI |
3.12.0 up to 3.18.2 | 3.18.2 |
djangorestframeworkDebian:12 |
All versions, plus listed versions 3.14.0-23.14.0-2+deb12u13.15.1-13.15.1-23.15.2-1 Show 9 moreShow less3.15.2-23.16.0-13.16.1-13.16.1-33.16.1-43.17.1+ds1-13.17.1-13.18.1+ds1-13.18.3+ds1-1 |
None known |
djangorestframeworkDebian:13 |
All versions, plus listed versions 3.16.0-13.16.1-13.16.1-33.16.1-43.17.1+ds1-13.17.1-13.18.1+ds1-13.18.3+ds1-1 |
None known |
djangorestframeworkDebian:14 |
before 3.18.3+ds1-1, plus listed versions 3.16.0-13.16.1-13.16.1-33.16.1-43.17.1+ds1-13.17.1-13.18.1+ds1-1 |
3.18.3+ds1-1 |
djangorestframeworkPyPI |
3.12.0 up to 3.18.2, plus listed versions 3.12.13.12.23.12.33.12.43.13.0 Show 12 moreShow less3.13.13.14.03.15.03.15.13.15.23.16.03.16.13.17.03.17.13.17.23.18.03.18.1 |
3.18.2 |
djangorestframeworkRoot:PyPI |
before 3.12.4+aikido.2 | 3.12.4+aikido.23.16.1+aikido.13.17.2+aikido.13.15.1+aikido.13.14.0+aikido.2 |
djangorestframeworkUbuntu:16.04:LTS |
All versions, plus listed versions 2.4.3-23.3.1-13.3.2-13.3.2-2 |
None known |
djangorestframeworkUbuntu:18.04:LTS |
All versions, plus listed versions 3.4.0-23.7.7-1 |
None known |
djangorestframeworkUbuntu:20.04:LTS |
All versions, plus listed versions 3.9.0-23.10.2-1 |
None known |
djangorestframeworkUbuntu:22.04:LTS |
All versions, plus listed versions 3.12.1-13.12.4-13.12.4-23.12.4-2ubuntu13.12.4-2.1ubuntu1 |
None known |
Show 4 moreShow less
| Package | Affected | Fixed |
|---|---|---|
djangorestframeworkUbuntu:24.04:LTS |
All versions, plus listed versions 3.14.0-2 |
None known |
djangorestframeworkUbuntu:26.10 |
All versions, plus listed versions 3.16.1-33.16.1-43.17.1+ds1-1 |
None known |
djangorestframeworkUbuntu:26.04:LTS |
All versions, plus listed versions 3.16.1-13.16.1-3 |
None known |
python-djangorestframeworkopenSUSE:Tumbleweed From an advisory covering 3 CVEs |
before 3.18.2-1.1 | 3.18.2-1.1 |
History
| Recorded | Changed | Sources |
|---|---|---|
| affected, facts | ||
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-114137 is a high vulnerability (CVSS 7.4) in Encode Django-rest-framework, published October 21, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.51%, the 43rd percentile. Fixed in djangorestframework 3.18.2.
References
github.com/encode/django-rest-framework/security/advisories/GHSA-xw6w-gcfp-cf8m
github.com/encode/django-rest-framework/commit/af3fabfd517307fbc5d500fba1ab8f6e25f220b0
github.com/encode/django-rest-framework/releases/tag/3.18.2
nvd.nist.gov/vuln/detail/CVE-2026-114137
github.com/advisories/GHSA-xw6w-gcfp-cf8m
github.com/CVEProject/cvelistV5/tree/main/cves/2026/114xxx/CVE-2026-114137.json
Left out here: the rest of 2 source descriptions. The JSON record has them.