File CVE-2024-1135.patch of Package python-gunicorn.38005
From 559caf920537ece2ef058e1de5e36af44756bb19 Mon Sep 17 00:00:00 2001
From: "Paul J. Dorn" <pajod@users.noreply.github.com>
Date: Wed, 6 Dec 2023 15:30:50 +0100
Subject: [PATCH 01/16] pytest: raise on malformed test fixtures
and unbreak test depending on backslash escape
---
tests/treq.py | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
Index: gunicorn-19.7.1/tests/treq.py
===================================================================
--- gunicorn-19.7.1.orig/tests/treq.py
+++ gunicorn-19.7.1/tests/treq.py
@@ -57,7 +57,9 @@ class request(object):
with open(self.fname, 'rb') as handle:
self.data = handle.read()
self.data = self.data.replace(b"\n", b"").replace(b"\\r\\n", b"\r\n")
- self.data = self.data.replace(b"\\0", b"\000")
+ self.data = self.data.replace(b"\\0", b"\000").replace(b"\\n", b"\n").replace(b"\\t", b"\t")
+ if b"\\" in self.data:
+ raise AssertionError("Unexpected backslash in test data - only handling HTAB, NUL and CRLF")
# Functions for sending data to the parser.
# These functions mock out reading from a
@@ -252,8 +254,10 @@ class request(object):
def check(self, cfg, sender, sizer, matcher):
cases = self.expect[:]
p = RequestParser(cfg, sender())
- for req in p:
+ parsed_request_idx = -1
+ for parsed_request_idx, req in enumerate(p):
self.same(req, sizer, matcher, cases.pop(0))
+ assert len(self.expect) == parsed_request_idx + 1
assert len(cases) == 0
def same(self, req, sizer, matcher, exp):
@@ -268,7 +272,8 @@ class request(object):
assert req.trailers == exp.get("trailers", [])
-class badrequest(object):
+class badrequest:
+ # FIXME: no good reason why this cannot match what the more extensive mechanism above
def __init__(self, fname):
self.fname = fname
self.name = os.path.basename(fname)
@@ -276,7 +281,9 @@ class badrequest(object):
with open(self.fname) as handle:
self.data = handle.read()
self.data = self.data.replace("\n", "").replace("\\r\\n", "\r\n")
- self.data = self.data.replace("\\0", "\000")
+ self.data = self.data.replace("\\0", "\000").replace("\\n", "\n").replace("\\t", "\t")
+ if "\\" in self.data:
+ raise AssertionError("Unexpected backslash in test data - only handling HTAB, NUL and CRLF")
self.data = self.data.encode('latin1')
def send(self):
@@ -289,4 +296,6 @@ class badrequest(object):
def check(self, cfg):
p = RequestParser(cfg, self.send())
- six.next(p)
+ # must fully consume iterator, otherwise EOF errors could go unnoticed
+ for _ in p:
+ pass
Index: gunicorn-19.7.1/gunicorn/http/body.py
===================================================================
--- gunicorn-19.7.1.orig/gunicorn/http/body.py
+++ gunicorn-19.7.1/gunicorn/http/body.py
@@ -49,7 +49,7 @@ class ChunkedReader(object):
if done:
unreader.unread(buf.getvalue()[2:])
return b""
- self.req.trailers = self.req.parse_headers(buf.getvalue()[:idx])
+ self.req.trailers = self.req.parse_headers(buf.getvalue()[:idx], from_trailer=True)
unreader.unread(buf.getvalue()[idx + 4:])
def parse_chunked(self, unreader):
@@ -83,11 +83,13 @@ class ChunkedReader(object):
data = buf.getvalue()
line, rest_chunk = data[:idx], data[idx + 2:]
- chunk_size = line.split(b";", 1)[0].strip()
- try:
- chunk_size = int(chunk_size, 16)
- except ValueError:
+ # RFC9112 7.1.1: BWS before chunk-ext - but ONLY then
+ chunk_size, *chunk_ext = line.split(b";", 1)
+ if chunk_ext:
+ chunk_size = chunk_size.rstrip(b" \t")
+ if any(n not in b"0123456789abcdefABCDEF" for n in chunk_size):
raise InvalidChunkSize(chunk_size)
+ chunk_size = int(chunk_size, 16)
if chunk_size == 0:
try:
Index: gunicorn-19.7.1/gunicorn/http/message.py
===================================================================
--- gunicorn-19.7.1.orig/gunicorn/http/message.py
+++ gunicorn-19.7.1/gunicorn/http/message.py
@@ -12,7 +12,7 @@ from gunicorn.http.unreader import Socke
from gunicorn.http.body import ChunkedReader, LengthReader, EOFReader, Body
from gunicorn.http.errors import (InvalidHeader, InvalidHeaderName, NoMoreData,
InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion,
- LimitRequestLine, LimitRequestHeaders)
+ LimitRequestLine, LimitRequestHeaders, UnsupportedTransferCoding)
from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest
from gunicorn.six import BytesIO
from gunicorn._compat import urlsplit
@@ -21,9 +21,15 @@ MAX_REQUEST_LINE = 8190
MAX_HEADERS = 32768
DEFAULT_MAX_HEADERFIELD_SIZE = 8190
-HEADER_RE = re.compile("[\x00-\x1F\x7F()<>@,;:\[\]={} \t\\\\\"]")
-METH_RE = re.compile(r"[A-Z0-9$-_.]{3,20}")
-VERSION_RE = re.compile(r"HTTP/(\d+)\.(\d+)")
+#HEADER_RE = re.compile("[\x00-\x1F\x7F()<>@,;:\[\]={} \t\\\\\"]")
+#METH_RE = re.compile(r"[A-Z0-9$-_.]{3,20}")
+#VERSION_RE = re.compile(r"HTTP/(\d+)\.(\d+)")
+# verbosely on purpose, avoid backslash ambiguity
+RFC9110_5_6_2_TOKEN_SPECIALS = r"!#$%&'*+-.^_`|~"
+TOKEN_RE = re.compile(r"[%s0-9a-zA-Z]+" % (re.escape(RFC9110_5_6_2_TOKEN_SPECIALS)))
+METHOD_BADCHAR_RE = re.compile("[a-z#]")
+# usually 1.0 or 1.1 - RFC9112 permits restricting to single-digit versions
+VERSION_RE = re.compile(r"HTTP/(\d)\.(\d)")
class Message(object):
@@ -34,6 +40,7 @@ class Message(object):
self.headers = []
self.trailers = []
self.body = None
+ self.must_close = False
# set headers limits
self.limit_request_fields = cfg.limit_request_fields
@@ -53,14 +60,17 @@ class Message(object):
self.unreader.unread(unused)
self.set_body_reader()
+ def force_close(self):
+ self.must_close = True
+
def parse(self):
raise NotImplementedError()
- def parse_headers(self, data):
+ def parse_headers(self, data, from_trailer=False):
headers = []
- # Split lines on \r\n keeping the \r\n on each line
- lines = [bytes_to_str(line) + "\r\n" for line in data.split(b"\r\n")]
+ # Split lines on \r\n
+ lines = [bytes_to_str(line) for line in data.split(b"\r\n")]
# Parse headers into key/value pairs paying attention
# to continuation lines.
@@ -70,28 +80,52 @@ class Message(object):
# Parse initial header name : value pair.
curr = lines.pop(0)
- header_length = len(curr)
- if curr.find(":") < 0:
- raise InvalidHeader(curr.strip())
+ header_length = len(curr) + len("\r\n")
+ if curr.find(":") <= 0:
+ raise InvalidHeader(curr)
name, value = curr.split(":", 1)
- name = name.rstrip(" \t").upper()
- if HEADER_RE.search(name):
+ name = name.rstrip(" \t")
+ if not TOKEN_RE.fullmatch(name):
raise InvalidHeaderName(name)
- name, value = name.strip(), [value.lstrip()]
+ # this is still a dangerous place to do this
+ # but it is more correct than doing it before the pattern match:
+ # after we entered Unicode wonderland, 8bits could case-shift into ASCII:
+ # b"\xDF".decode("latin-1").upper().encode("ascii") == b"SS"
+ name = name.upper()
+
+ value = [value.lstrip(" \t")]
# Consume value continuation lines
while len(lines) and lines[0].startswith((" ", "\t")):
curr = lines.pop(0)
- header_length += len(curr)
+ header_length += len(curr) + len("\r\n")
if header_length > self.limit_request_field_size > 0:
raise LimitRequestHeaders("limit request headers "
+ "fields size")
- value.append(curr)
- value = ''.join(value).rstrip()
+ value.append(curr.strip("\t "))
+ value = " ".join(value)
if header_length > self.limit_request_field_size > 0:
raise LimitRequestHeaders("limit request headers fields size")
+
+ # ambiguous mapping allows fooling downstream, e.g. merging non-identical headers:
+ # X-Forwarded-For: 2001:db8::ha:cc:ed
+ # X_Forwarded_For: 127.0.0.1,::1
+ # HTTP_X_FORWARDED_FOR = 2001:db8::ha:cc:ed,127.0.0.1,::1
+ # Only modify after fixing *ALL* header transformations; network to wsgi env
+ if "_" in name:
+ if self.cfg.header_map == "dangerous":
+ # as if we did not know we cannot safely map this
+ pass
+ elif self.cfg.header_map == "drop":
+ # almost as if it never had been there
+ # but still counts against resource limits
+ continue
+ else:
+ # fail-safe fallthrough: refuse
+ raise InvalidHeaderName(name)
+
headers.append((name, value))
return headers
@@ -102,11 +136,50 @@ class Message(object):
if name == "CONTENT-LENGTH":
content_length = value
elif name == "TRANSFER-ENCODING":
- chunked = value.lower() == "chunked"
+ if value.lower() == "chunked":
+ # DANGER: transer codings stack, and stacked chunking is never intended
+ if chunked:
+ raise InvalidHeader("TRANSFER-ENCODING", req=self)
+ chunked = True
+ elif value.lower() == "identity":
+ # does not do much, could still plausibly desync from what the proxy does
+ # safe option: nuke it, its never needed
+ if chunked:
+ raise InvalidHeader("TRANSFER-ENCODING", req=self)
+ elif value.lower() == "":
+ # lacking security review on this case
+ # offer the option to restore previous behaviour, but refuse by default, for now
+ self.force_close()
+ if not self.cfg.tolerate_dangerous_framing:
+ raise UnsupportedTransferCoding(value)
+ # DANGER: do not change lightly; ref: request smuggling
+ # T-E is a list and we *could* support correctly parsing its elements
+ # .. but that is only safe after getting all the edge cases right
+ # .. for which no real-world need exists, so best to NOT open that can of worms
+ else:
+ self.force_close()
+ # even if parser is extended, retain this branch:
+ # the "chunked not last" case remains to be rejected!
+ raise UnsupportedTransferCoding(value)
elif name == "SEC-WEBSOCKET-KEY1":
content_length = 8
if chunked:
+ # two potentially dangerous cases:
+ # a) CL + TE (TE overrides CL.. only safe if the recipient sees it that way too)
+ # b) chunked HTTP/1.0 (always faulty)
+ if self.version < (1, 1):
+ # framing wonky, see RFC 9112 Section 6.1
+ self.force_close()
+ if not self.cfg.tolerate_dangerous_framing:
+ raise InvalidHeader("TRANSFER-ENCODING", req=self)
+ if content_length is not None:
+ # we cannot be certain the message framing we understood matches proxy intent
+ # -> whatever happens next, remaining input must not be trusted
+ self.force_close()
+ # either processing or rejecting is permitted in RFC 9112 Section 6.1
+ if not self.cfg.tolerate_dangerous_framing:
+ raise InvalidHeader("CONTENT-LENGTH", req=self)
self.body = Body(ChunkedReader(self, self.unreader))
elif content_length is not None:
try:
@@ -122,9 +195,11 @@ class Message(object):
self.body = Body(EOFReader(self.unreader))
def should_close(self):
+ if self.must_close:
+ return True
for (h, v) in self.headers:
if h == "CONNECTION":
- v = v.lower().strip()
+ v = v.lower().strip(" \t")
if v == "close":
return True
elif v == "keep-alive":
@@ -198,7 +273,7 @@ class Request(Message):
self.unreader.unread(data[2:])
return b""
- self.headers = self.parse_headers(data[:idx])
+ self.headers = self.parse_headers(data[:idx], from_trailer=False)
ret = data[idx + 4:]
buf = BytesIO()
@@ -257,7 +332,7 @@ class Request(Message):
raise ForbiddenProxyRequest(remote_host)
def parse_proxy_protocol(self, line):
- bits = line.split()
+ bits = line.split(" ")
if len(bits) != 6:
raise InvalidProxyLine(line)
@@ -302,14 +377,28 @@ class Request(Message):
}
def parse_request_line(self, line):
- bits = line.split(None, 2)
+ line_bytes = line.encode()
+ bits = line.split(" ", 2)
if len(bits) != 3:
raise InvalidRequestLine(line)
- # Method
- if not METH_RE.match(bits[0]):
- raise InvalidRequestMethod(bits[0])
- self.method = bits[0].upper()
+ # Method: RFC9110 Section 9
+ self.method = bits[0]
+
+ # nonstandard restriction, suitable for all IANA registered methods
+ # partially enforced in previous gunicorn versions
+ if not self.cfg.permit_unconventional_http_method:
+ if METHOD_BADCHAR_RE.search(self.method):
+ raise InvalidRequestMethod(self.method)
+ if not 3 <= len(bits[0]) <= 20:
+ raise InvalidRequestMethod(self.method)
+ # standard restriction: RFC9110 token
+ if not TOKEN_RE.fullmatch(self.method):
+ raise InvalidRequestMethod(self.method)
+ # nonstandard and dangerous
+ # methods are merely uppercase by convention, no case-insensitive treatment is intended
+ if self.cfg.casefold_http_method:
+ self.method = self.method.upper()
# URI
# When the path starts with //, urlsplit considers it as a
@@ -331,10 +420,14 @@ class Request(Message):
self.fragment = parts.fragment or ""
# Version
- match = VERSION_RE.match(bits[2])
+ match = VERSION_RE.fullmatch(bits[2])
if match is None:
raise InvalidHTTPVersion(bits[2])
self.version = (int(match.group(1)), int(match.group(2)))
+ if not (1, 0) <= self.version < (2, 0):
+ # if ever relaxing this, carefully review Content-Encoding processing
+ if not self.cfg.permit_unconventional_http_version:
+ raise InvalidHTTPVersion(self.version)
def set_body_reader(self):
super(Request, self).set_body_reader()
Index: gunicorn-19.7.1/gunicorn/http/wsgi.py
===================================================================
--- gunicorn-19.7.1.orig/gunicorn/http/wsgi.py
+++ gunicorn-19.7.1/gunicorn/http/wsgi.py
@@ -10,8 +10,8 @@ import re
import sys
from gunicorn._compat import unquote_to_wsgi_str
-from gunicorn.http.message import HEADER_RE
-from gunicorn.http.errors import InvalidHeader, InvalidHeaderName
+from gunicorn.http.message import TOKEN_RE
+from gunicorn.http.errors import ConfigurationProblem, InvalidHeader, InvalidHeaderName
from gunicorn.six import string_types, binary_type, reraise
from gunicorn import SERVER_SOFTWARE
import gunicorn.util as util
@@ -29,7 +29,9 @@ except ImportError:
# with sending files in blocks over 2GB.
BLKSIZE = 0x3FFFFFFF
-HEADER_VALUE_RE = re.compile(r'[\x00-\x1F\x7F]')
+# RFC9110 5.5: field-vchar = VCHAR / obs-text
+# RFC4234 B.1: VCHAR = 0x21-x07E = printable ASCII
+HEADER_VALUE_RE = re.compile(r'[ \t\x21-\x7e\x80-\xff]*')
log = logging.getLogger(__name__)
@@ -151,6 +153,8 @@ def create(req, sock, client, server, cf
environ['CONTENT_LENGTH'] = hdr_value
continue
+ # do not change lightly, this is a common source of security problems
+ # RFC9110 Section 17.10 discourages ambiguous or incomplete mappings
key = 'HTTP_' + hdr_name.replace('-', '_')
if key in environ:
hdr_value = "%s,%s" % (environ[key], hdr_value)
@@ -198,7 +202,11 @@ def create(req, sock, client, server, cf
# set the path and script name
path_info = req.path
if script_name:
- path_info = path_info.split(script_name, 1)[1]
+ if not path_info.startswith(script_name):
+ raise ConfigurationProblem(
+ "Request path %r does not start with SCRIPT_NAME %r" %
+ (path_info, script_name))
+ path_info = path_info[len(script_name):]
environ['PATH_INFO'] = unquote_to_wsgi_str(path_info)
environ['SCRIPT_NAME'] = script_name
@@ -267,28 +275,29 @@ class Response(object):
if not isinstance(name, string_types):
raise TypeError('%r is not a string' % name)
- if HEADER_RE.search(name):
+ if not TOKEN_RE.fullmatch(name):
raise InvalidHeaderName('%r' % name)
- if HEADER_VALUE_RE.search(value):
+ if not HEADER_VALUE_RE.fullmatch(value):
raise InvalidHeader('%r' % value)
- value = str(value).strip()
- lname = name.lower().strip()
+ # RFC9110 5.5
+ value = str(value).strip(" \t")
+ lname = name.lower()
if lname == "content-length":
self.response_length = int(value)
elif util.is_hoppish(name):
if lname == "connection":
# handle websocket
- if value.lower().strip() == "upgrade":
+ if value.lower() == "upgrade":
self.upgrade = True
elif lname == "upgrade":
- if value.lower().strip() == "websocket":
- self.headers.append((name.strip(), value))
+ if value.lower() == "websocket":
+ self.headers.append((name, value))
# ignore hopbyhop headers
continue
- self.headers.append((name.strip(), value))
+ self.headers.append((name, value))
def is_chunked(self):
# Only use chunked responses when the client is
Index: gunicorn-19.7.1/tests/requests/invalid/003.http
===================================================================
--- gunicorn-19.7.1.orig/tests/requests/invalid/003.http
+++ gunicorn-19.7.1/tests/requests/invalid/003.http
@@ -1,2 +1,2 @@
--blargh /foo HTTP/1.1\r\n
-\r\n
\ No newline at end of file
+GET\n/\nHTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/003.py
===================================================================
--- gunicorn-19.7.1.orig/tests/requests/invalid/003.py
+++ gunicorn-19.7.1/tests/requests/invalid/003.py
@@ -1,2 +1,2 @@
-from gunicorn.http.errors import InvalidRequestMethod
-request = InvalidRequestMethod
\ No newline at end of file
+from gunicorn.http.errors import InvalidRequestLine
+request = InvalidRequestLine
Index: gunicorn-19.7.1/tests/requests/valid/031.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/031.http
@@ -0,0 +1,2 @@
+-BLARGH /foo HTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/valid/031.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/031.py
@@ -0,0 +1,7 @@
+request = {
+ "method": "-BLARGH",
+ "uri": uri("/foo"),
+ "version": (1, 1),
+ "headers": [],
+ "body": b""
+}
Index: gunicorn-19.7.1/gunicorn/http/errors.py
===================================================================
--- gunicorn-19.7.1.orig/gunicorn/http/errors.py
+++ gunicorn-19.7.1/gunicorn/http/errors.py
@@ -16,6 +16,15 @@ class NoMoreData(IOError):
return "No more data after: %r" % self.buf
+class ConfigurationProblem(ParseException):
+ def __init__(self, info):
+ self.info = info
+ self.code = 500
+
+ def __str__(self):
+ return "Configuration problem: %s" % self.info
+
+
class InvalidRequestLine(ParseException):
def __init__(self, req):
self.req = req
@@ -58,6 +67,15 @@ class InvalidHeaderName(ParseException):
return "Invalid HTTP header name: %r" % self.hdr
+class UnsupportedTransferCoding(ParseException):
+ def __init__(self, hdr):
+ self.hdr = hdr
+ self.code = 501
+
+ def __str__(self):
+ return "Unsupported transfer coding: %r" % self.hdr
+
+
class InvalidChunkSize(IOError):
def __init__(self, data):
self.data = data
Index: gunicorn-19.7.1/SECURITY.md
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/SECURITY.md
@@ -0,0 +1,22 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+**Please note that public Github issues are open for everyone to see!**
+
+If you believe you are found a problem in Gunicorn software, examples or documentation, we encourage you to send your report privately via email, or via Github using the *Report a vulnerability* button in the [Security](https://github.com/benoitc/gunicorn/security) section.
+
+## Supported Releases
+
+At this time, **only the latest release** receives any security attention whatsoever.
+
+| Version | Status |
+| ------- | ------------------ |
+| latest release | :white_check_mark: |
+| 21.2.0 | :x: |
+| 20.0.0 | :x: |
+| < 20.0 | :x: |
+
+## Python Versions
+
+Gunicorn runs on Python 3.7+, we *highly recommend* the latest release of a [supported series](https://devguide.python.org/version/) and will not prioritize issues exclusively affecting in EoL environments.
Index: gunicorn-19.7.1/tests/test_http.py
===================================================================
--- gunicorn-19.7.1.orig/tests/test_http.py
+++ gunicorn-19.7.1/tests/test_http.py
@@ -9,6 +9,17 @@ from gunicorn.http.wsgi import Response
from gunicorn.http.unreader import Unreader, IterUnreader, SocketUnreader
from gunicorn.six import BytesIO
from gunicorn.http.errors import InvalidHeader, InvalidHeaderName
+from gunicorn.http.message import TOKEN_RE
+
+
+def test_method_pattern():
+ assert TOKEN_RE.fullmatch("GET")
+ assert TOKEN_RE.fullmatch("MKCALENDAR")
+ assert not TOKEN_RE.fullmatch("GET:")
+ assert not TOKEN_RE.fullmatch("GET;")
+ RFC9110_5_6_2_TOKEN_DELIM = r'"(),/:;<=>?@[\]{}'
+ for bad_char in RFC9110_5_6_2_TOKEN_DELIM:
+ assert not TOKEN_RE.match(bad_char)
try:
import unittest.mock as mock
Index: gunicorn-19.7.1/gunicorn/config.py
===================================================================
--- gunicorn-19.7.1.orig/gunicorn/config.py
+++ gunicorn-19.7.1/gunicorn/config.py
@@ -1858,3 +1858,127 @@ class PasteGlobalConf(Setting):
.. versionadded:: 19.7
"""
+
+
+class PermitUnconventionalHTTPMethod(Setting):
+ name = "permit_unconventional_http_method"
+ section = "Server Mechanics"
+ cli = ["--permit-unconventional-http-method"]
+ validator = validate_bool
+ action = "store_true"
+ default = False
+ desc = """\
+ Permit HTTP methods not matching conventions, such as IANA registration guidelines
+
+ This permits request methods of length less than 3 or more than 20,
+ methods with lowercase characters or methods containing the # character.
+ HTTP methods are case sensitive by definition, and merely uppercase by convention.
+
+ This option is provided to diagnose backwards-incompatible changes.
+
+ Use with care and only if necessary. May be removed in a future version.
+
+ .. versionadded:: 22.0.0
+ """
+
+
+class PermitUnconventionalHTTPVersion(Setting):
+ name = "permit_unconventional_http_version"
+ section = "Server Mechanics"
+ cli = ["--permit-unconventional-http-version"]
+ validator = validate_bool
+ action = "store_true"
+ default = False
+ desc = """\
+ Permit HTTP version not matching conventions of 2023
+
+ This disables the refusal of likely malformed request lines.
+ It is unusual to specify HTTP 1 versions other than 1.0 and 1.1.
+
+ This option is provided to diagnose backwards-incompatible changes.
+ Use with care and only if necessary. May be removed in a future version.
+
+ .. versionadded:: 22.0.0
+ """
+
+
+class CasefoldHTTPMethod(Setting):
+ name = "casefold_http_method"
+ section = "Server Mechanics"
+ cli = ["--casefold-http-method"]
+ validator = validate_bool
+ action = "store_true"
+ default = False
+ desc = """\
+ Transform received HTTP methods to uppercase
+
+ HTTP methods are case sensitive by definition, and merely uppercase by convention.
+
+ This option is provided because previous versions of gunicorn defaulted to this behaviour.
+
+ Use with care and only if necessary. May be removed in a future version.
+
+ .. versionadded:: 22.0.0
+ """
+
+
+def validate_header_map_behaviour(val):
+ # FIXME: refactor all of this subclassing stdlib argparse
+
+ if val is None:
+ return
+
+ if not isinstance(val, str):
+ raise TypeError("Invalid type for casting: %s" % val)
+ if val.lower().strip() == "drop":
+ return "drop"
+ elif val.lower().strip() == "refuse":
+ return "refuse"
+ elif val.lower().strip() == "dangerous":
+ return "dangerous"
+ else:
+ raise ValueError("Invalid header map behaviour: %s" % val)
+
+
+class HeaderMap(Setting):
+ name = "header_map"
+ section = "Server Mechanics"
+ cli = ["--header-map"]
+ validator = validate_header_map_behaviour
+ default = "drop"
+ desc = """\
+ Configure how header field names are mapped into environ
+
+ Headers containing underscores are permitted by RFC9110,
+ but gunicorn joining headers of different names into
+ the same environment variable will dangerously confuse applications as to which is which.
+
+ The safe default ``drop`` is to silently drop headers that cannot be unambiguously mapped.
+ The value ``refuse`` will return an error if a request contains *any* such header.
+ The value ``dangerous`` matches the previous, not advisabble, behaviour of mapping different
+ header field names into the same environ name.
+
+ Use with care and only if necessary and after considering if your problem could
+ instead be solved by specifically renaming or rewriting only the intended headers
+ on a proxy in front of Gunicorn.
+
+ .. versionadded:: 22.0.0
+ """
+
+
+class TolerateDangerousFraming(Setting):
+ name = "tolerate_dangerous_framing"
+ section = "Server Mechanics"
+ cli = ["--tolerate-dangerous-framing"]
+ validator = validate_bool
+ action = "store_true"
+ default = False
+ desc = """\
+ Process requests with both Transfer-Encoding and Content-Length
+
+ This is known to induce vulnerabilities, but not strictly forbidden by RFC9112.
+
+ Use with care and only if necessary. May be removed in a future version.
+
+ .. versionadded:: 22.0.0
+ """
Index: gunicorn-19.7.1/tests/requests/invalid/003b.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/003b.http
@@ -0,0 +1,2 @@
+bla:rgh /foo HTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/003b.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/003b.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidRequestMethod
+request = InvalidRequestMethod
\ No newline at end of file
Index: gunicorn-19.7.1/tests/requests/invalid/003c.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/003c.http
@@ -0,0 +1,2 @@
+-bl /foo HTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/003c.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/003c.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidRequestMethod
+request = InvalidRequestMethod
Index: gunicorn-19.7.1/tests/requests/valid/031compat.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/031compat.http
@@ -0,0 +1,2 @@
+-blargh /foo HTTP/1.1\r\n
+\r\n
\ No newline at end of file
Index: gunicorn-19.7.1/tests/requests/valid/031compat.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/031compat.py
@@ -0,0 +1,13 @@
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("permit_unconventional_http_method", True)
+cfg.set("casefold_http_method", True)
+
+request = {
+ "method": "-BLARGH",
+ "uri": uri("/foo"),
+ "version": (1, 1),
+ "headers": [],
+ "body": b""
+}
Index: gunicorn-19.7.1/tests/requests/valid/031compat2.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/031compat2.http
@@ -0,0 +1,2 @@
+-blargh /foo HTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/valid/031compat2.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/031compat2.py
@@ -0,0 +1,12 @@
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("permit_unconventional_http_method", True)
+
+request = {
+ "method": "-blargh",
+ "uri": uri("/foo"),
+ "version": (1, 1),
+ "headers": [],
+ "body": b""
+}
Index: gunicorn-19.7.1/tests/requests/invalid/040.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/040.http
@@ -0,0 +1,6 @@
+GET /keep/same/as?invalid/040 HTTP/1.0\r\n
+Transfer_Encoding: tricked\r\n
+Content-Length: 7\r\n
+Content_Length: -1E23\r\n
+\r\n
+tricked\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/040.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/040.py
@@ -0,0 +1,7 @@
+from gunicorn.http.errors import InvalidHeaderName
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("header_map", "refuse")
+
+request = InvalidHeaderName
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_07.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_07.http
@@ -0,0 +1,10 @@
+POST /chunked_ambiguous_header_mapping HTTP/1.1\r\n
+Transfer_Encoding: gzip\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+0\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_07.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_07.py
@@ -0,0 +1,7 @@
+from gunicorn.http.errors import InvalidHeaderName
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("header_map", "refuse")
+
+request = InvalidHeaderName
Index: gunicorn-19.7.1/tests/requests/valid/040.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/040.http
@@ -0,0 +1,6 @@
+GET /keep/same/as?invalid/040 HTTP/1.0\r\n
+Transfer_Encoding: tricked\r\n
+Content-Length: 7\r\n
+Content_Length: -1E23\r\n
+\r\n
+tricked\r\n
Index: gunicorn-19.7.1/tests/requests/valid/040.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/040.py
@@ -0,0 +1,9 @@
+request = {
+ "method": "GET",
+ "uri": uri("/keep/same/as?invalid/040"),
+ "version": (1, 0),
+ "headers": [
+ ("CONTENT-LENGTH", "7")
+ ],
+ "body": b'tricked'
+}
Index: gunicorn-19.7.1/tests/requests/valid/040_compat.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/040_compat.http
@@ -0,0 +1,6 @@
+GET /keep/same/as?invalid/040 HTTP/1.0\r\n
+Transfer_Encoding: tricked\r\n
+Content-Length: 7\r\n
+Content_Length: -1E23\r\n
+\r\n
+tricked\r\n
Index: gunicorn-19.7.1/tests/requests/valid/040_compat.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/040_compat.py
@@ -0,0 +1,16 @@
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("header_map", "dangerous")
+
+request = {
+ "method": "GET",
+ "uri": uri("/keep/same/as?invalid/040"),
+ "version": (1, 0),
+ "headers": [
+ ("TRANSFER_ENCODING", "tricked"),
+ ("CONTENT-LENGTH", "7"),
+ ("CONTENT_LENGTH", "-1E23"),
+ ],
+ "body": b'tricked'
+}
Index: gunicorn-19.7.1/gunicorn/workers/base.py
===================================================================
--- gunicorn-19.7.1.orig/gunicorn/workers/base.py
+++ gunicorn-19.7.1/gunicorn/workers/base.py
@@ -233,6 +233,8 @@ class Worker(object):
else:
if hasattr(req, "uri"):
self.log.exception("Error handling request %s", req.uri)
+ else:
+ self.log.exception("Error handling request (no URI read)")
status_int = 500
reason = "Internal Server Error"
mesg = ""
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_01.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_01.http
@@ -0,0 +1,12 @@
+POST /chunked_w_underscore_chunk_size HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+6_0\r\n
+ world\r\n
+0\r\n
+\r\n
+POST /after HTTP/1.1\r\n
+Transfer-Encoding: identity\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_01.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_01.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidChunkSize
+request = InvalidChunkSize
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_02.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_02.http
@@ -0,0 +1,9 @@
+POST /chunked_with_prefixed_value HTTP/1.1\r\n
+Content-Length: 12\r\n
+Transfer-Encoding: \tchunked\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_02.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_02.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidHeader
+request = InvalidHeader
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_03.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_03.http
@@ -0,0 +1,8 @@
+POST /double_chunked HTTP/1.1\r\n
+Transfer-Encoding: identity, chunked, identity, chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_03.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_03.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import UnsupportedTransferCoding
+request = UnsupportedTransferCoding
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_04.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_04.http
@@ -0,0 +1,11 @@
+POST /chunked_twice HTTP/1.1\r\n
+Transfer-Encoding: identity\r\n
+Transfer-Encoding: chunked\r\n
+Transfer-Encoding: identity\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_04.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_04.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidHeader
+request = InvalidHeader
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_05.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_05.http
@@ -0,0 +1,11 @@
+POST /chunked_HTTP_1.0 HTTP/1.0\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+0\r\n
+Vary: *\r\n
+Content-Type: text/plain\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_05.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_05.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidHeader
+request = InvalidHeader
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_06.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_06.http
@@ -0,0 +1,9 @@
+POST /chunked_not_last HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+Transfer-Encoding: gzip\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_06.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_06.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import UnsupportedTransferCoding
+request = UnsupportedTransferCoding
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_08.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_08.http
@@ -0,0 +1,9 @@
+POST /chunked_not_last HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+Transfer-Encoding: identity\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_08.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_08.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidHeader
+request = InvalidHeader
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_01.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_01.http
@@ -0,0 +1,4 @@
+GETß /germans.. HTTP/1.1\r\n
+Content-Length: 3\r\n
+\r\n
+ÄÄÄ
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_01.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_01.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidRequestMethod
+
+cfg = Config()
+request = InvalidRequestMethod
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_02.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_02.http
@@ -0,0 +1,4 @@
+GETÿ /french.. HTTP/1.1\r\n
+Content-Length: 3\r\n
+\r\n
+ÄÄÄ
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_02.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_02.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidRequestMethod
+
+cfg = Config()
+request = InvalidRequestMethod
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_04.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_04.http
@@ -0,0 +1,5 @@
+GET /french.. HTTP/1.1\r\n
+Content-Lengthÿ: 3\r\n
+Content-Length: 3\r\n
+\r\n
+ÄÄÄ
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_04.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_04.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidHeaderName
+
+cfg = Config()
+request = InvalidHeaderName
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_01.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_01.http
@@ -0,0 +1,2 @@
+GET\0PROXY /foo HTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_01.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_01.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidRequestMethod
+request = InvalidRequestMethod
\ No newline at end of file
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_02.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_02.http
@@ -0,0 +1,2 @@
+GET\0 /foo HTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_02.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_02.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidRequestMethod
+request = InvalidRequestMethod
\ No newline at end of file
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_03.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_03.http
@@ -0,0 +1,4 @@
+GET /stuff/here?foo=bar HTTP/1.1\r\n
+Content-Length: 0 1\r\n
+\r\n
+x
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_03.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_03.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidHeader
+
+cfg = Config()
+request = InvalidHeader
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_04.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_04.http
@@ -0,0 +1,5 @@
+GET /stuff/here?foo=bar HTTP/1.1\r\n
+Content-Length: 3 1\r\n
+\r\n
+xyz
+abc123
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_04.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_04.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidHeader
+
+cfg = Config()
+request = InvalidHeader
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_05.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_05.http
@@ -0,0 +1,4 @@
+GET: /stuff/here?foo=bar HTTP/1.1\r\n
+Content-Length: 3\r\n
+\r\n
+xyz
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_05.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_05.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidRequestMethod
+
+cfg = Config()
+request = InvalidRequestMethod
Index: gunicorn-19.7.1/tests/requests/valid/025.http
===================================================================
--- gunicorn-19.7.1.orig/tests/requests/valid/025.http
+++ gunicorn-19.7.1/tests/requests/valid/025.http
@@ -1,10 +1,9 @@
POST /chunked_cont_h_at_first HTTP/1.1\r\n
-Content-Length: -1\r\n
Transfer-Encoding: chunked\r\n
\r\n
5; some; parameters=stuff\r\n
hello\r\n
-6; blahblah; blah\r\n
+6 \t;\tblahblah; blah\r\n
world\r\n
0\r\n
\r\n
@@ -16,4 +15,10 @@ Content-Length: -1\r\n
hello\r\n
6; blahblah; blah\r\n
world\r\n
-0\r\n
\ No newline at end of file
+0\r\n
+\r\n
+PUT /ignored_after_dangerous_framing HTTP/1.1\r\n
+Content-Length: 3\r\n
+\r\n
+foo\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/valid/025.py
===================================================================
--- gunicorn-19.7.1.orig/tests/requests/valid/025.py
+++ gunicorn-19.7.1/tests/requests/valid/025.py
@@ -1,9 +1,13 @@
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("tolerate_dangerous_framing", True)
+
req1 = {
"method": "POST",
"uri": uri("/chunked_cont_h_at_first"),
"version": (1, 1),
"headers": [
- ("CONTENT-LENGTH", "-1"),
("TRANSFER-ENCODING", "chunked")
],
"body": b"hello world"
Index: gunicorn-19.7.1/tests/requests/valid/025compat.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/025compat.http
@@ -0,0 +1,18 @@
+POST /chunked_cont_h_at_first HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5; some; parameters=stuff\r\n
+hello\r\n
+6; blahblah; blah\r\n
+ world\r\n
+0\r\n
+\r\n
+PUT /chunked_cont_h_at_last HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+Content-Length: -1\r\n
+\r\n
+5; some; parameters=stuff\r\n
+hello\r\n
+6; blahblah; blah\r\n
+ world\r\n
+0\r\n
Index: gunicorn-19.7.1/tests/requests/valid/025compat.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/025compat.py
@@ -0,0 +1,27 @@
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("tolerate_dangerous_framing", True)
+
+req1 = {
+ "method": "POST",
+ "uri": uri("/chunked_cont_h_at_first"),
+ "version": (1, 1),
+ "headers": [
+ ("TRANSFER-ENCODING", "chunked")
+ ],
+ "body": b"hello world"
+}
+
+req2 = {
+ "method": "PUT",
+ "uri": uri("/chunked_cont_h_at_last"),
+ "version": (1, 1),
+ "headers": [
+ ("TRANSFER-ENCODING", "chunked"),
+ ("CONTENT-LENGTH", "-1"),
+ ],
+ "body": b"hello world"
+}
+
+request = [req1, req2]
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_03.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_03.http
@@ -0,0 +1,5 @@
+GET /germans.. HTTP/1.1\r\n
+Content-Lengthß: 3\r\n
+Content-Length: 3\r\n
+\r\n
+ÄÄÄ
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_03.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_03.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidHeaderName
+
+cfg = Config()
+request = InvalidHeaderName
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_06.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_06.http
@@ -0,0 +1,4 @@
+GET /the/future HTTP/1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111.1\r\n
+Content-Length: 7\r\n
+\r\n
+Old Man
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_06.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_06.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidHTTPVersion
+
+cfg = Config()
+request = InvalidHTTPVersion
Index: gunicorn-19.7.1/tests/requests/invalid/version_01.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/version_01.http
@@ -0,0 +1,2 @@
+GET /foo HTTP/0.99\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/version_01.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/version_01.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidHTTPVersion
+request = InvalidHTTPVersion
Index: gunicorn-19.7.1/tests/requests/invalid/version_02.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/version_02.http
@@ -0,0 +1,2 @@
+GET /foo HTTP/2.0\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/version_02.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/version_02.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidHTTPVersion
+request = InvalidHTTPVersion
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_09.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_09.http
@@ -0,0 +1,7 @@
+POST /chunked_ows_without_ext HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+0 \r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_09.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_09.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidChunkSize
+request = InvalidChunkSize
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_10.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_10.http
@@ -0,0 +1,7 @@
+POST /chunked_ows_before HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+ 0\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_10.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_10.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidChunkSize
+request = InvalidChunkSize
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_11.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_11.http
@@ -0,0 +1,7 @@
+POST /chunked_ows_before HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\n;\r\n
+hello\r\n
+0\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_11.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_11.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidChunkSize
+request = InvalidChunkSize
Index: gunicorn-19.7.1/tests/requests/valid/016.py
===================================================================
--- gunicorn-19.7.1.orig/tests/requests/valid/016.py
+++ gunicorn-19.7.1/tests/requests/valid/016.py
@@ -1,35 +1,35 @@
-certificate = """-----BEGIN CERTIFICATE-----\r\n
- MIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n
- ETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n
- AkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n
- dWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n
- SzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n
- BAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n
- BQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n
- W51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n
- gW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n
- 0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n
- u2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n
- wgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n
- 1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n
- BglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n
- VR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n
- loCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n
- aWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n
- 9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n
- IjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n
- BgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n
- cHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4Qg\r\n
- EDBDAWLmh0dHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC\r\n
- 5jcmwwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n
- Y3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n
- XCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n
- UO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n
- hTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n
- wTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n
- Yhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n
- RA==\r\n
- -----END CERTIFICATE-----""".replace("\n\n", "\n")
+certificate = """-----BEGIN CERTIFICATE-----
+ MIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx
+ ETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT
+ AkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu
+ dWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV
+ SzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV
+ BAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB
+ BQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF
+ W51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR
+ gW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL
+ 0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP
+ u2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR
+ wgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG
+ 1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs
+ BglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD
+ VR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj
+ loCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj
+ aWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG
+ 9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE
+ IjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO
+ BgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1
+ cHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4Qg
+ EDBDAWLmh0dHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC
+ 5jcmwwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv
+ Y3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3
+ XCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8
+ UO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk
+ hTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK
+ wTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu
+ Yhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3
+ RA==
+ -----END CERTIFICATE-----""".replace("\n", "")
request = {
"method": "GET",