From bdb2ebd5a4913fff1e92800f3763e4a879526d3e Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Thu, 26 Mar 2026 15:08:54 +0100 Subject: [PATCH] Reject chunk extensions with bare CR bytes (RFC 9112) Both WSGI and ASGI parsers now validate that chunk extensions do not contain bare CR characters, which are not allowed per RFC 9112. Fixes #3556 --- docs/content/2026-news.md | 3 +++ docs/content/news.md | 3 +++ gunicorn/asgi/parser.py | 4 ++++ gunicorn/http/body.py | 5 ++++- gunicorn/http/errors.py | 10 ++++++++++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/content/2026-news.md b/docs/content/2026-news.md index c2aa73eb..8799fc5d 100644 --- a/docs/content/2026-news.md +++ b/docs/content/2026-news.md @@ -15,6 +15,9 @@ - **HTTP/2 Documentation**: Fix `http_protocols` examples to use comma-separated string instead of list syntax ([#3561](https://github.com/benoitc/gunicorn/issues/3561)) +- **Chunked Encoding**: Reject chunk extensions containing bare CR bytes per RFC 9112 + ([#3556](https://github.com/benoitc/gunicorn/discussions/3556)) + ### Security - **ASGI Parser Header Validation**: Add security checks per RFC 9110/9112: diff --git a/docs/content/news.md b/docs/content/news.md index 1505fb85..648f29f5 100644 --- a/docs/content/news.md +++ b/docs/content/news.md @@ -15,6 +15,9 @@ - **HTTP/2 Documentation**: Fix `http_protocols` examples to use comma-separated string instead of list syntax ([#3561](https://github.com/benoitc/gunicorn/issues/3561)) +- **Chunked Encoding**: Reject chunk extensions containing bare CR bytes per RFC 9112 + ([#3556](https://github.com/benoitc/gunicorn/discussions/3556)) + ### Security - **ASGI Parser Header Validation**: Add security checks per RFC 9110/9112: diff --git a/gunicorn/asgi/parser.py b/gunicorn/asgi/parser.py index a1f608a6..63a34fdc 100644 --- a/gunicorn/asgi/parser.py +++ b/gunicorn/asgi/parser.py @@ -670,6 +670,10 @@ class PythonProtocol: # Handle chunk extensions (e.g., "5;ext=value") semicolon = size_line.find(b';') if semicolon != -1: + # RFC 9112: chunk-ext must not contain bare CR + chunk_ext = size_line[semicolon + 1:] + if b'\r' in chunk_ext: + raise ParseError("Invalid chunk extension: bare CR not allowed") size_line = size_line[:semicolon] # Strict validation: reject leading/trailing whitespace diff --git a/gunicorn/http/body.py b/gunicorn/http/body.py index d7ee29e7..e433baee 100644 --- a/gunicorn/http/body.py +++ b/gunicorn/http/body.py @@ -6,7 +6,7 @@ import io import sys from gunicorn.http.errors import (NoMoreData, ChunkMissingTerminator, - InvalidChunkSize) + InvalidChunkSize, InvalidChunkExtension) class ChunkedReader: @@ -90,6 +90,9 @@ class ChunkedReader: # RFC9112 7.1.1: BWS before chunk-ext - but ONLY then chunk_size, *chunk_ext = line.split(b";", 1) if chunk_ext: + # RFC 9112: chunk-ext must not contain bare CR + if b'\r' in chunk_ext[0]: + raise InvalidChunkExtension("bare CR not allowed") chunk_size = chunk_size.rstrip(b" \t") if any(n not in b"0123456789abcdefABCDEF" for n in chunk_size): raise InvalidChunkSize(chunk_size) diff --git a/gunicorn/http/errors.py b/gunicorn/http/errors.py index f3ba534e..f6c7d579 100644 --- a/gunicorn/http/errors.py +++ b/gunicorn/http/errors.py @@ -113,6 +113,16 @@ class ChunkMissingTerminator(IOError): return "Invalid chunk terminator is not '\\r\\n': %r" % self.term +class InvalidChunkExtension(IOError): + """Invalid chunk extension per RFC 9112.""" + + def __init__(self, reason): + self.reason = reason + + def __str__(self): + return "Invalid chunk extension: %s" % self.reason + + class LimitRequestLine(ParseException): def __init__(self, size, max_size=None): self.size = size