# Django REST framework: JSONParser applies an attacker-named codec to the request stream

DRF's JSONParser passes the client-supplied Content-Type charset to codecs.getreader, so charset=bz2_codec decompresses a 108-byte body into 50MB, bypassing DATA_UPLOAD_MAX_MEMORY_SIZE.

- Payout: Research
- Severity: high
- Program: Django REST framework
- Found by: syrull, jisung-02
- Disclosed: 2026-10-22
- CVE: CVE-2026-114136
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-3547-9m27-7rxg
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-3547-9m27-7rxg
- Tags: area:web domain:api domain:web impact:dos kind:research platform:ghsa sev:high tech:python technique:parser-differential technique:source-review vuln:dos vuln:misconfiguration

**Target:** DRF JSONParser / any JSON-accepting API endpoint
**Root cause:** JSONParser.parse calls codecs.getreader(encoding)(stream) with the attacker-controlled charset; getreader is the one codecs entry point that ignores CodecInfo._is_text_encoding, so byte-transform codecs like bz2_codec are accepted. Django's DATA_UPLOAD_MAX_MEMORY_SIZE is charged against wire bytes in HttpRequest.body before the parser decompresses, so expansion is uncounted.
**Steps:**
1. Send `Content-Type: application/json; charset=bz2_codec` with a bz2-compressed JSON body, e.g. `{"a":"AAAA..."}` compressed to 108 bytes.
2. Django resolves charset to request.encoding and checks DATA_UPLOAD_MAX_MEMORY_SIZE against the 108 wire bytes, passing.
3. DRF JSONParser wraps io.BytesIO(self.body) in codecs.getreader('bz2_codec') and json.load decompresses the whole stream.
4. View receives a 50,331,648-character string; peak RSS grows ~97 MiB per request.
5. Repeat concurrently to exhaust worker memory; zlib_codec works identically.
**How found:** Noticed JSONParser decodes the stream with the client-supplied charset via codecs.getreader, then enumerated the ~455 codec names codecs.lookup resolves and found ~18 byte transforms (bz2_codec, zlib_codec, base64_codec, hex_codec, quopri_codec, uu_codec) that getreader accepts because it skips the _is_text_encoding check. Traced that Django's size limit runs on wire bytes in HttpRequest.body before decompression, and confirmed FormParser/MultiPartParser fail closed while JSONParser amplifies.
**Exploitation:** Unauthenticated POST to any endpoint accepting JSON with default parser set; a few hundred bytes per request allocates hundreds of MB, so a small number of concurrent requests exhausts a worker process. Memory is per-request and released after, so impact is availability only.
**Insight:** When a parser takes a client-named encoding, check whether it uses codecs.getreader/open rather than bytes.decode/str(): only the former accepts non-text byte-transform codecs (bz2, zlib, base64, hex, quopri, uu), turning any size limit measured on wire bytes into a decompression bomb. Also check whether the size guard runs before or after decoding.

## Proof of concept (copied exactly from the original report)

**bz2_codec charset request body amplification**

```
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"),
}
```

**Compressed payload construction with bz2/zlib**

```
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()}
```

**Request dispatch with attacker-named codec**

```
request = RequestFactory().generic(
        "POST", "/x", data=body, content_type=content_type
    )
    try:
        direct = {"status_code": build_view().as_view()(request).status_code}
    except BaseException as exc:  # noqa: BLE001 - naming the class is the point
        direct = {"exception": f"{type(exc).__module__}.{type(exc).__name__}"}
```

