diff --git a/gunicorn/http/body.py b/gunicorn/http/body.py index d7ee29e78..01ad4637d 100644 --- a/gunicorn/http/body.py +++ b/gunicorn/http/body.py @@ -6,7 +6,7 @@ import sys from gunicorn.http.errors import (NoMoreData, ChunkMissingTerminator, - InvalidChunkSize) + InvalidChunkSize, InvalidChunkExtension) class ChunkedReader: @@ -91,6 +91,10 @@ def parse_chunk_size(self, unreader, data=None): chunk_size, *chunk_ext = line.split(b";", 1) if chunk_ext: chunk_size = chunk_size.rstrip(b" \t") + # Security: Don't allow CRs and LFs in chunk extensions + # This can cause request smuggling issues with some proxies + if any(c in chunk_ext[0] for c in (b"\n", b"\r")): + raise InvalidChunkExtension(chunk_ext[0]) if any(n not in b"0123456789abcdefABCDEF" for n in chunk_size): raise InvalidChunkSize(chunk_size) if len(chunk_size) == 0: diff --git a/gunicorn/http/errors.py b/gunicorn/http/errors.py index bcb970072..8ed1d0b75 100644 --- a/gunicorn/http/errors.py +++ b/gunicorn/http/errors.py @@ -97,6 +97,14 @@ def __str__(self): return "Invalid chunk size: %r" % self.data +class InvalidChunkExtension(IOError): + def __init__(self, data): + self.data = data + + def __str__(self): + return "Invalid chunk extension: %r" % self.data + + class ChunkMissingTerminator(IOError): def __init__(self, term): self.term = term diff --git a/tests/requests/invalid/chunked_14.http b/tests/requests/invalid/chunked_14.http new file mode 100644 index 000000000..e51e15d95 --- /dev/null +++ b/tests/requests/invalid/chunked_14.http @@ -0,0 +1,7 @@ +POST /chunked_newline_in_chunk_ext HTTP/1.1\r\n +Transfer-Encoding: chunked\r\n +\r\n +5;foo\nbar\r\n +hello\r\n +0\r\n +\r\n diff --git a/tests/requests/invalid/chunked_14.py b/tests/requests/invalid/chunked_14.py new file mode 100644 index 000000000..14ef63b43 --- /dev/null +++ b/tests/requests/invalid/chunked_14.py @@ -0,0 +1,2 @@ +from gunicorn.http.errors import InvalidChunkExtension +request = InvalidChunkExtension