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
This commit is contained in:
Benoit Chesneau 2026-03-26 15:08:54 +01:00
parent 7057fc9f89
commit bdb2ebd5a4
5 changed files with 24 additions and 1 deletions

View File

@ -15,6 +15,9 @@
- **HTTP/2 Documentation**: Fix `http_protocols` examples to use comma-separated string - **HTTP/2 Documentation**: Fix `http_protocols` examples to use comma-separated string
instead of list syntax ([#3561](https://github.com/benoitc/gunicorn/issues/3561)) 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 ### Security
- **ASGI Parser Header Validation**: Add security checks per RFC 9110/9112: - **ASGI Parser Header Validation**: Add security checks per RFC 9110/9112:

View File

@ -15,6 +15,9 @@
- **HTTP/2 Documentation**: Fix `http_protocols` examples to use comma-separated string - **HTTP/2 Documentation**: Fix `http_protocols` examples to use comma-separated string
instead of list syntax ([#3561](https://github.com/benoitc/gunicorn/issues/3561)) 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 ### Security
- **ASGI Parser Header Validation**: Add security checks per RFC 9110/9112: - **ASGI Parser Header Validation**: Add security checks per RFC 9110/9112:

View File

@ -670,6 +670,10 @@ class PythonProtocol:
# Handle chunk extensions (e.g., "5;ext=value") # Handle chunk extensions (e.g., "5;ext=value")
semicolon = size_line.find(b';') semicolon = size_line.find(b';')
if semicolon != -1: 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] size_line = size_line[:semicolon]
# Strict validation: reject leading/trailing whitespace # Strict validation: reject leading/trailing whitespace

View File

@ -6,7 +6,7 @@ import io
import sys import sys
from gunicorn.http.errors import (NoMoreData, ChunkMissingTerminator, from gunicorn.http.errors import (NoMoreData, ChunkMissingTerminator,
InvalidChunkSize) InvalidChunkSize, InvalidChunkExtension)
class ChunkedReader: class ChunkedReader:
@ -90,6 +90,9 @@ class ChunkedReader:
# RFC9112 7.1.1: BWS before chunk-ext - but ONLY then # RFC9112 7.1.1: BWS before chunk-ext - but ONLY then
chunk_size, *chunk_ext = line.split(b";", 1) chunk_size, *chunk_ext = line.split(b";", 1)
if chunk_ext: 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") chunk_size = chunk_size.rstrip(b" \t")
if any(n not in b"0123456789abcdefABCDEF" for n in chunk_size): if any(n not in b"0123456789abcdefABCDEF" for n in chunk_size):
raise InvalidChunkSize(chunk_size) raise InvalidChunkSize(chunk_size)

View File

@ -113,6 +113,16 @@ class ChunkMissingTerminator(IOError):
return "Invalid chunk terminator is not '\\r\\n': %r" % self.term 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): class LimitRequestLine(ParseException):
def __init__(self, size, max_size=None): def __init__(self, size, max_size=None):
self.size = size self.size = size