Repository navigation
⬆️(dependencies) update djangorestframework to v3.18.2 [SECURITY] - #2902
Merged
Merged
Conversation
lunika
approved these changes
Oct 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
==3.18.1→==3.18.2Django REST framework: JSONParser applies an attacker-named codec to the request stream
CVE-2026-114136 / GHSA-3547-9m27-7rxg
More information
Details
Summary
An unauthenticated attacker can defeat Django's
DATA_UPLOAD_MAX_MEMORY_SIZElimit and force aworker process to allocate arbitrary memory, by naming a compression codec in the Content-Type
charset parameter. A 108-byte request body produces a 50,331,648-character value inside the view, an
amplification factor of 466,033, and grows process peak RSS by about 97 MiB. No application
configuration is required beyond DRF's default parser set; the endpoint only has to accept a JSON
body.
Details
JSONParser.parsedecodes the request body with the charset the client supplied on the Content-Typeheader:
Of the roughly 455 codec names
codecs.lookupresolves on CPython 3.14, about 18 are byte transformsinstead of character sets:
bz2_codec,zlib_codec,base64_codec,hex_codec,quopri_codec,uu_codecand similar.codecs.getreaderis the only entry point in thecodecsmodule that doesnot consult
CodecInfo._is_text_encoding, so it accepts all of them. SendingContent-Type: application/json; charset=bz2_codecwraps the request stream in a bz2 decompressorbefore
json.loadreads it.The ordering is what makes this exploitable. Django charges
DATA_UPLOAD_MAX_MEMORY_SIZEagainst thebytes that arrived on the wire:
rest_framework/request.pyhandsJSONParseranio.BytesIO(self.body), andHttpRequest.bodyis where_check_data_too_big()runs. Decompressionhappens afterwards, inside the parser, where nothing is counting.
This is DRF's defect and not Django's. Django resolves the charset parameter and assigns
request.encoding, andcodecs.lookupdoes resolve byte transforms, but every Django consumer ofrequest.encodinggoes through the text-codec API and fails closed. Bothbytes.decode('bz2_codec')and
str(raw, 'bz2_codec')raiseLookupError: 'bz2_codec' is not a text encoding; use codecs.decode() to handle arbitrary codecs.DRF is the only caller that hands the request stream to
codecs.getreader.FormParser(parsers.py:84) andMultiPartParser(parsers.py:104-118) take the same client-namedencoding but fail closed instead of amplifying.
The sink was introduced by
9a228116(PR #5147, 2017-05-24, "Improve memory footprint when readinglarge JSON requests"), first released in 3.6.4. The final comment on that PR observes the intended
memory gain does not hold because
read()decodes the whole stream, which is the same property thatmakes this expansion unbounded. No
_is_text_encodingcheck has been added since.A one-line fix at
parsers.py:61-64:PoC
Save as
poc_charset_amplification.pyand run it. It builds its own payloads, runs each arm in afresh subprocess (peak RSS is a high-water mark, so measuring several arms in one process reports
near-zero growth for every arm after the first), and prints a verdict.
Observed output, abbreviated to the summary block:
C1 establishes that the size limit is live and configured, so the 200 in T is a defeated guard and
not an absent one. C1 reports its own RSS growth because the client holds the 48 MiB body before the
limit rejects it, which is client-side cost and never reaches the view. C2 carries most of the
weight: its body is byte-identical to T, and only the codec name differs. RSS figures vary by a few
MiB between runs.
Repeating all four arms on Django 5.2.17 LTS gave 109 wire bytes to 50,331,648 characters, 461,758x,
with C1 and C2 both holding.
One note if you re-implement the request yourself: use
Client.generic()orRequestFactory().generic(). BothClient.post()andRequestFactory.post()push the body throughforce_bytes(data, encoding=<charset>)(django/test/client.py:438-447), which destroys a compressedpayload before the parser sees it.
Impact
Unauthenticated denial of service through memory amplification, against any endpoint that accepts a
JSON body with the default parser set. Every deployment on 3.6.4 or later is affected, and no
application code or configuration choice opts into it. Memory is per-request and released afterwards,
which is why this is rated on availability alone rather than as Critical, but the amplification factor
means a small number of concurrent requests can exhaust a worker. A few hundred bytes allocates
hundreds of megabytes, and nothing in DRF or Django bounds the expanded size.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Django REST framework: Accept header with many large quoted parameters burns ~19s of CPU per request in content negotiation
CVE-2026-114138 / GHSA-33wh-fxxf-88vv
More information
Details
Summary
DRF's content negotiation parses each Accept token several times per request and never limits the number of tokens or their total size. Each parse ends up in Django's parse_header_parameters, which takes quadratic time when a quoted parameter value is packed with semicolons. On my machine a single GET with a 780KB Accept header made of 96 such tokens kept one gunicorn worker busy for 18.87 seconds (Django 6.1, djangorestframework 3.18.0, gunicorn 26.0.0, Python 3.13, Docker on macOS). The header is syntactically valid, fits inside gunicorn's default header limits, and needs no authentication. Two concurrent requests, which is one per sync worker, made a default two worker deployment stop answering for my whole 40 second test window. It recovered half a second after I stopped sending.
Details
What happens
get_accept_list() splits the Accept header on commas with no cap on token count or total size, at rest_framework/negotiation.py:96-97.
For every token, select_renderer() calls media_type_matches() once per renderer, and each call builds fresh _MediaType objects for both sides, at rest_framework/negotiation.py:53-56 and rest_framework/utils/mediatypes.py:22-23.
_MediaType.__init__calls django.utils.http.parse_header_parameters unconditionally, at rest_framework/utils/mediatypes.py:47.When nothing matches, DRF raises NotAcceptable (406). Rendering that error response also needs a renderer, so finalize_response() runs the entire negotiation a second time through perform_content_negotiation(force=True), at rest_framework/views.py:434-437.
With the default two renderers this comes to about six parses per Accept token per request. I confirmed the count by instrumenting _MediaType with a counter: six per token, three from the normal pass and three from the forced re-negotiation.
The quadratic part is on the Django side. parse_header_parameters splits parameters with _parseparam, which handles quoted values by recounting the quote characters from the start of the string for every candidate separator, the s.count('"', 0, end) calls in django/utils/http.py. A quoted value holding k semicolons costs roughly k rescans over k characters, so a single ~8KB value takes about 31ms per parse here.
Numbers from my runs, measured as utime+stime deltas of the server process:
one token, ~9KB: 230ms server CPU
24 tokens, 216KB: 5.07s on runserver, which has no per field limit
48 tokens, 432KB: 10.08s on runserver
96 tokens, 780KB: 18.87s under gunicorn defaults
Wall time matched CPU time in every run, so this is pure CPU work. RSS stayed flat at 57MB throughout, this is a time attack and not a memory one.
A few details that matter for reproducing it:
Django rejects a single media type string longer than 10,000 chars (MAX_HEADER_LENGTH), so each token has to stay just under that. Going over gives you a ValueError and a 500, which is a different and much weaker problem.
gunicorn defaults are limit_request_field_size 8190 and limit_request_fields 100, so the working shape is 96 separate Accept fields with one ~8.1KB token each. That puts the single request ceiling near 19s, below the default 30s worker timeout, so one request alone does not get a stock gunicorn worker killed by the arbiter. It does not need to. Each request pins one sync worker for its full duration, so on two workers, two held connections wedge the service.
order_by_precedence collects tokens into a set, so identical tokens collapse and the cost mostly disappears. Each token needs its own parameter name, x0=, x1=, x2= and so on.
Where a fix could go, either of these alone would be enough:
Parse each Accept token once per request and reuse the result in the matching loop and in the 406 re-negotiation.
Put an aggregate budget in get_accept_list(), say 8KB total or 64 tokens, and treat anything past it as not acceptable.
The quadratic behavior itself lives in Django's parser, but DRF is the one calling it six times per token with no cap on how many tokens there are, so both fixes look like DRF changes to me.
PoC
Verified end to end on Docker Desktop for macOS (Apple Silicon, 8 cores), python:3.13-slim image, Django 6.1, djangorestframework 3.18.0, gunicorn 26.0.0. Nothing was configured beyond defaults.
Minimal target, any default DRF project works, this is the one I used.
requirements.txt:
pocproj/settings.py:
pocproj/api.py:
pocproj/urls.py:
Start it:
Client, stdlib only, run with python3:
Impact
Availability only, no confidentiality or integrity impact. Any endpoint the attacker can reach is affected, public ones with no authentication at all. On a stock gunicorn setup with two sync workers, two held connections are enough to stop the service for as long as the attacker keeps them, and one request alone costs about 19 seconds of one core. Recovery is immediate once the attack stops, there is no lasting damage.
The requests are valid HTTP and get answered with 406, so they show up in access logs, but nothing about them is malformed, so filters that look for bad requests will not catch them. DRF throttling runs before content negotiation and would cut the rate if configured, but the default configuration ships with no throttle classes, and each request that does get through still costs the full 19 seconds.
The negotiation structure, meaning the uncapped token split, the per-renderer re-parsing, and the 406 re-negotiation, has been in place since 2012. The call switched to parse_header_parameters in July 2022 in #8556. I verified the behavior on 3.18.0.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Django REST framework: TokenProxy.delete() reports success and revokes nothing
CVE-2026-114137 / GHSA-xw6w-gcfp-cf8m
More information
Details
Summary
Deleting an authentication token through
TokenProxysilently deletes nothing. The call raises noexception 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
TokenProxyinstance believes it has revoked a token that is still live.Details
TokenProxyoverridespkto return the user id (rest_framework/authtoken/models.py:49-63):The concrete
Tokenmodel keys onkey, not on an integer id.Model.delete()builds its deletequery from
pk, so it emits: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_modelre-fetches the concrete row:and
TokenAdminsetsactions = Nonewith 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.TokenProxywas added bye2bd3b6a(PR #7341, "Adjusted token admin to map to user ID"), firstreleased in 3.12.0.
A fix would drop the
pkoverride, or giveTokenProxyadelete()that operates on the concreterow, or raise instead of silently reporting zero deletions.
PoC
Save as
poc_tokenproxy_revocation.pyand run it.Observed output:
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
TokenProxyinstance, andreceives 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. Exploitationdepends on the deployment revoking through the proxy, which is a natural thing to do given
TokenProxyis the model registered in the admin and the one most code will reach for. Anydeployment on 3.12.0 or later that revokes tokens outside DRF's own
TokenAdmin.delete_modelshouldaudit whether its revocations took effect.
Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Release Notes
encode/django-rest-framework (djangorestframework)
v3.18.2Compare Source
What's Changed
Security
Acceptheaders by @jisung-02 in 148ab65. Thanks @jisung-02 for the report.TokenProxydeletion actually delete the token by @syrull in af3fabf. Thanks @syrull for the report.Features
Bug fixes
blank=Truefields in unique constraints being treated as required by @MehrazRumman in #10039UniqueConstraintvalidation with conditional fields by @majidkhazaei in #10021New Contributors
Full Changelog: encode/django-rest-framework@3.18.1...3.18.2
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.