Skip to content

Commit

Permalink
Merge pull request #423 from kenballus/main
Browse files Browse the repository at this point in the history
Validate HTTP versions and methods
  • Loading branch information
digitalresistor authored Feb 4, 2024
2 parents 1bee8d3 + 5acce4f commit 4f9af4d
Show file tree
Hide file tree
Showing 10 changed files with 36 additions and 42 deletions.
1 change: 0 additions & 1 deletion src/waitress/adjustments.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,6 @@ class Adjustments:
server_name = "waitress.invalid"

def __init__(self, **kw):

if "listen" in kw and ("host" in kw or "port" in kw):
raise ValueError("host or port may not be set if listen is set.")

Expand Down
1 change: 0 additions & 1 deletion src/waitress/buffers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@


class FileBasedBuffer:

remain = 0

def __init__(self, file, from_buffer=None):
Expand Down
48 changes: 24 additions & 24 deletions src/waitress/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ def parse_header(self, header_plus):

# command, uri, version will be bytes
command, uri, version = crack_first_line(first_line)
if command == uri == version == b"":
raise ParsingError("Start line is invalid")

# self.request_uri is like nginx's request_uri:
# "full original request URI (with arguments)"
self.request_uri = uri.decode("latin-1")
Expand Down Expand Up @@ -407,36 +410,33 @@ def get_header_lines(header):


first_line_re = re.compile(
b"([^ ]+) "
b"((?:[^ :?#]+://[^ ?#/]*(?:[0-9]{1,5})?)?[^ ]+)"
b"(( HTTP/([0-9.]+))$|$)"
rb"(?P<method>[!#$%&'*+\-.^_`|~0-9A-Za-z]+) "
rb"(?P<uri>(?:[^ :?#]+://[^ ?#/]*(?:[0-9]{1,5})?)?[^ ]+)"
rb"(?: HTTP/(?P<version>[0-9]\.[0-9]))?"
)


def crack_first_line(line):
m = first_line_re.match(line)
m = first_line_re.fullmatch(line)

if m is not None and m.end() == len(line):
if m.group(3):
version = m.group(5)
else:
version = b""
method = m.group(1)
if m is None:
return b"", b"", b""

# the request methods that are currently defined are all uppercase:
# https://www.iana.org/assignments/http-methods/http-methods.xhtml and
# the request method is case sensitive according to
# https://tools.ietf.org/html/rfc7231#section-4.1
version = m["version"] or b""
method = m["method"]
uri = m["uri"]

# By disallowing anything but uppercase methods we save poor
# unsuspecting souls from sending lowercase HTTP methods to waitress
# and having the request complete, while servers like nginx drop the
# request onto the floor.
# the request methods that are currently defined are all uppercase:
# https://www.iana.org/assignments/http-methods/http-methods.xhtml and
# the request method is case sensitive according to
# https://tools.ietf.org/html/rfc7231#section-4.1

if method != method.upper():
raise ParsingError('Malformed HTTP method "%s"' % str(method, "latin-1"))
uri = m.group(2)
# By disallowing anything but uppercase methods we save poor
# unsuspecting souls from sending lowercase HTTP methods to waitress
# and having the request complete, while servers like nginx drop the
# request onto the floor.

return method, uri, version
else:
return b"", b"", b""
if method != method.upper():
raise ParsingError('Malformed HTTP method "%s"' % str(method, "latin-1"))

return method, uri, version
2 changes: 0 additions & 2 deletions src/waitress/receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@


class FixedStreamReceiver:

# See IStreamConsumer
completed = False
error = None
Expand Down Expand Up @@ -61,7 +60,6 @@ def getbuf(self):


class ChunkedReceiver:

chunk_remainder = 0
validate_chunk_end = False
control_line = b""
Expand Down
3 changes: 1 addition & 2 deletions src/waitress/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ def close(self):


class BaseWSGIServer(wasyncore.dispatcher):

channel_class = HTTPChannel
next_channel_cleanup = 0
socketmod = socket # test shim
Expand Down Expand Up @@ -372,7 +371,7 @@ def getsockname(self):
)

def set_socket_options(self, conn):
for (level, optname, value) in self.adj.socket_options:
for level, optname, value in self.adj.socket_options:
conn.setsockopt(level, optname, value)


Expand Down
2 changes: 1 addition & 1 deletion src/waitress/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def build_response_header(self):
server_header = None
connection_close_header = None

for (headername, headerval) in self.response_headers:
for headername, headerval in self.response_headers:
headername = "-".join([x.capitalize() for x in headername.split("-")])

if headername == "Content-Length":
Expand Down
2 changes: 2 additions & 0 deletions src/waitress/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ def unpack_rfc822(m):
)

rfc850_reg = re.compile(rfc850_date)


# they actually unpack the same way
def unpack_rfc850(m):
g = m.group
Expand Down
1 change: 0 additions & 1 deletion src/waitress/wasyncore.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,6 @@ def compact_traceback():


class dispatcher:

debug = False
connected = False
accepting = False
Expand Down
4 changes: 0 additions & 4 deletions tests/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ def __init__(self, application, queue, **kw): # pragma: no cover


class SubprocessTests:

exe = sys.executable

server = None
Expand Down Expand Up @@ -133,7 +132,6 @@ def send_check_error(self, to_send):


class TcpTests(SubprocessTests):

server = FixtureTcpWSGIServer

def make_http_connection(self):
Expand Down Expand Up @@ -924,7 +922,6 @@ def test_no_content_length(self):


class TooLargeTests:

toobig = 1050

def setUp(self):
Expand Down Expand Up @@ -1606,7 +1603,6 @@ def __init__(self, application, queue, **kw): # pragma: no cover
queue.put(self.socket.getsockname())

class UnixTests(SubprocessTests):

server = FixtureUnixWSGIServer

def make_http_connection(self):
Expand Down
14 changes: 8 additions & 6 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@

class TestHTTPRequestParser(unittest.TestCase):
def setUp(self):

my_adj = Adjustments()
self.parser = HTTPRequestParser(my_adj)

Expand Down Expand Up @@ -98,7 +97,6 @@ def test_received_already_completed(self):
self.assertEqual(result, 0)

def test_received_cl_too_large(self):

self.parser.adj.max_request_body_size = 2
data = b"GET /foobar HTTP/8.4\r\nContent-Length: 10\r\n\r\n"
result = self.parser.received(data)
Expand All @@ -107,7 +105,6 @@ def test_received_cl_too_large(self):
self.assertTrue(isinstance(self.parser.error, RequestEntityTooLarge))

def test_received_headers_not_too_large_multiple_chunks(self):

data = b"GET /foobar HTTP/8.4\r\nX-Foo: 1\r\n"
data2 = b"X-Foo-Other: 3\r\n\r\n"
self.parser.adj.max_request_header_size = len(data) + len(data2) + 1
Expand All @@ -119,7 +116,6 @@ def test_received_headers_not_too_large_multiple_chunks(self):
self.assertFalse(self.parser.error)

def test_received_headers_too_large(self):

self.parser.adj.max_request_header_size = 2
data = b"GET /foobar HTTP/8.4\r\nX-Foo: 1\r\n\r\n"
result = self.parser.received(data)
Expand Down Expand Up @@ -252,7 +248,6 @@ def test_parse_header_transfer_encoding_invalid(self):
self.assertTrue(False)

def test_parse_header_transfer_encoding_invalid_multiple(self):

data = b"GET /foobar HTTP/1.1\r\ntransfer-encoding: gzip\r\ntransfer-encoding: chunked\r\n"

try:
Expand Down Expand Up @@ -571,6 +566,14 @@ def test_crack_first_line_missing_version(self):
result = self._callFUT(b"GET /")
self.assertEqual(result, (b"GET", b"/", b""))

def test_crack_first_line_bad_method(self):
result = self._callFUT(b"GE\x00 /foobar HTTP/8.4")
self.assertEqual(result, (b"", b"", b""))

def test_crack_first_line_bad_version(self):
result = self._callFUT(b"GET /foobar HTTP/.1.")
self.assertEqual(result, (b"", b"", b""))


class TestHTTPRequestParserIntegration(unittest.TestCase):
def setUp(self):
Expand Down Expand Up @@ -713,7 +716,6 @@ def testSpoofedHeadersDropped(self):

class Test_unquote_bytes_to_wsgi(unittest.TestCase):
def _callFUT(self, v):

return unquote_bytes_to_wsgi(v)

def test_highorder(self):
Expand Down

0 comments on commit 4f9af4d

Please sign in to comment.