psf-drf-check(8)
Django REST framework October 2026: checker with mitigation (v1)
CVE-2026-114138 (Accept header CPU exhaustion)
CVE-2026-114137 (TokenProxy.delete() revokes nothing)
If this software is helpful, consider a donation to our 501c3 nonprofit:
https://privacysafe.foundation
Django REST framework (DRF) is the standard toolkit for building web APIs on
Django. Both flaws are fixed in DRF 3.18.2.
CVE-2026-114138 DefaultContentNegotiation accepts an unbounded number and size
of Accept header tokens, and parses each one repeatedly for
every renderer. A remote, unauthenticated client can send many
distinct media types with large quoted parameters and burn
about 19 seconds of CPU per request. Every API view does
content negotiation, so any reachable endpoint is a target.
CVSS 7.5. Affected: every release before 3.18.2 (the 0.x and
2.x lines are not fixed upstream). The cost also involves
Django's own header parameter parser, which Django fixed
separately in 5.2.18, 6.0.9 and 6.1.2, so keep Django current
as well.
CVE-2026-114137 From 3.12.0 until 3.18.2, TokenProxy.delete() in
rest_framework.authtoken uses the wrong primary key, so the
delete matches no row and reports success. The token keeps
working. DRF tokens do not expire, so code that revokes access
through a TokenProxy instance leaves the access open
indefinitely. CVSS 7.4. Only sites with rest_framework.authtoken
installed are exposed, and DRF's own admin delete is not
affected. Revocation done with Token.objects (not TokenProxy)
works on every version.
"check" is read-only; "mitigate" and "rollback" are the only commands that change
anything. "check" finds installed copies (dist-info and egg-info
metadata) and declared pins (requirements, poetry.lock, uv.lock, pdm.lock,
Pipfile.lock), notes where rest_framework.authtoken and TokenProxy appear in
project source, and for installed copies compares the neighboring Django.
Usage:
./drf-check.sh [command] [DIR ...]
check (default) scan DIR (default /usr/lib /usr/local/lib /opt /srv /var/www
/var/lib /home /root). Exit 0 = nothing affected, 1 = an
affected copy or pin was found.
mitigate [options] (root) changes the host. PrivacySafe protection: real Accept headers are far shorter than 1 KB, so
nginx answers 406 to an Accept header of 1024 characters or more,
which bounds the cost of the attack. Only when an affected copy is
found (--force overrides). Adds a map block right after "http {" and
an include as the first line of every server block that proxies
(proxy_pass, uwsgi_pass or scgi_pass), then checks with nginx -t; if
nginx rejects the result everything is undone at once. nginx only.
--server-name NAME only the server block that lists NAME (repeat)
--dir DIR where to look for Django REST framework (repeat)
--dry-run show the changes, change nothing
--reload graceful reload afterwards (systemd)
--force apply even if no affected copy is found
rollback [--dry-run] [--reload] remove what mitigate added and restore the files it edited.
version print the script version.
Containers: with no directory argument the check also reads the project files it needs from
running docker and podman containers (SCAN_CONTAINERS=1, the default; 0 skips them), through a
read-only probe, and judges them like host projects. The copies are temporary: a path shown as
.../docker_NAME/PATH is the PATH inside the container NAME.
PSF_LOCK_WAIT seconds a run that changes the host waits for another one that is already
changing it (default 30), then stops with exit 2; the lock is STATE_DIR/.lockThe Security Patches software and this documentation are dedicated to the public domain under CC0 1.0 Universal. Other pages on privacysafe.app/docs are CC BY-SA 4.0 unless the page says otherwise.