# Django REST framework: TokenProxy.delete() reports success and revokes nothing

DRF's TokenProxy overrides pk with user_id, so .delete() emits a DELETE matching a user id against the key column, silently revoking nothing while the token stays valid.

- Payout: Research
- Severity: high
- Program: Django REST framework
- Found by: syrull
- Disclosed: 2026-10-22
- CVE: CVE-2026-114137
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-xw6w-gcfp-cf8m
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-xw6w-gcfp-cf8m
- Tags: area:web domain:web impact:ato impact:integrity kind:research platform:ghsa sev:high tech:python technique:source-review vuln:access-control vuln:auth-bypass

**Target:** authtoken TokenProxy model / token revocation path
**Root cause:** TokenProxy.pk returns user_id while the concrete Token model keys on the 40-char key column; Model.delete() builds its query from pk, so the DELETE matches a user id against the key column and deletes zero rows without raising.
**Steps:**
1. Install djangorestframework >= 3.12.0, < 3.18.2 with rest_framework.authtoken.
2. Create a user and token via `Token.objects.create(user=user)`, confirm `GET /` with `authorization: Token <key>` returns 200.
3. Fetch `TokenProxy.objects.get(user=user)` and call `.delete()`; observe SQL `DELETE FROM "authtoken_token" WHERE "authtoken_token"."key" IN ('1')`, return `(0, {'authtoken.TokenProxy': 0})`, no exception.
4. Re-probe the same token: still 200, row count unchanged.
5. Control: `Token.objects.get(key=key).delete()` returns `(1, ...)`, row gone, probe returns 401.
**How found:** Noticed TokenProxy overrides pk to return user_id while Token keys on key, then traced Model.delete()'s pk-based query construction and confirmed the mismatched DELETE with a query-capturing PoC plus a concrete-model control.
**Exploitation:** Not remotely exploitable; requires the deployment to revoke tokens via a TokenProxy instance (natural since TokenProxy is the admin-registered model). The revocation silently no-ops, leaving a live non-expiring credential with whoever holds it.
**Insight:** When a proxy model overrides pk to a foreign identity, every inherited queryset/instance method (delete, update, refresh) silently targets the wrong column; grep for `@property def pk` overrides and test delete/update paths, not just reads.

