From dc07ef1ed9a4f2570244ca6abb042bd0cf2bfc6d Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Sun, 11 Mar 2018 18:34:32 -0400 Subject: [PATCH] httputil: Clean up error handling in HTTPHeaders.parse Fixes #2280 --- tornado/http1connection.py | 7 +------ tornado/httputil.py | 13 ++++++++++++- tornado/test/httpserver_test.py | 2 +- tornado/test/httputil_test.py | 8 ++++++++ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/tornado/http1connection.py b/tornado/http1connection.py index ed906223a..0349e171a 100644 --- a/tornado/http1connection.py +++ b/tornado/http1connection.py @@ -519,12 +519,7 @@ class HTTP1Connection(httputil.HTTPConnection): # RFC 7230 section allows for both CRLF and bare LF. eol = data.find("\n") start_line = data[:eol].rstrip("\r") - try: - headers = httputil.HTTPHeaders.parse(data[eol:]) - except ValueError: - # probably form split() if there was no ':' in the line - raise httputil.HTTPInputError("Malformed HTTP headers: %r" % - data[eol:100]) + headers = httputil.HTTPHeaders.parse(data[eol:]) return start_line, headers def _read_body(self, code, headers, delegate): diff --git a/tornado/httputil.py b/tornado/httputil.py index ceff73509..3d2d3359d 100644 --- a/tornado/httputil.py +++ b/tornado/httputil.py @@ -183,11 +183,16 @@ class HTTPHeaders(collections.MutableMapping): """ if line[0].isspace(): # continuation of a multi-line header + if self._last_key is None: + raise HTTPInputError("first header line cannot start with whitespace") new_part = ' ' + line.lstrip() self._as_list[self._last_key][-1] += new_part self._dict[self._last_key] += new_part else: - name, value = line.split(":", 1) + try: + name, value = line.split(":", 1) + except ValueError: + raise HTTPInputError("no colon in header line") self.add(name, value.strip()) @classmethod @@ -197,6 +202,12 @@ class HTTPHeaders(collections.MutableMapping): >>> h = HTTPHeaders.parse("Content-Type: text/html\\r\\nContent-Length: 42\\r\\n") >>> sorted(h.items()) [('Content-Length', '42'), ('Content-Type', 'text/html')] + + .. versionchanged:: 5.1 + + Raises `HTTPInputError` on malformed headers instead of a + mix of `KeyError`, and `ValueError`. + """ h = cls() for line in _CRLF_RE.split(headers): diff --git a/tornado/test/httpserver_test.py b/tornado/test/httpserver_test.py index a626369b4..30696a04a 100644 --- a/tornado/test/httpserver_test.py +++ b/tornado/test/httpserver_test.py @@ -425,7 +425,7 @@ class HTTPServerRawTest(AsyncHTTPTestCase): self.wait() def test_malformed_headers(self): - with ExpectLog(gen_log, '.*Malformed HTTP headers'): + with ExpectLog(gen_log, '.*Malformed HTTP message.*no colon in header line'): self.stream.write(b'GET / HTTP/1.0\r\nasdf\r\n\r\n') self.io_loop.add_timeout(datetime.timedelta(seconds=0.05), self.stop) diff --git a/tornado/test/httputil_test.py b/tornado/test/httputil_test.py index 5c064ddea..bd5832bcd 100644 --- a/tornado/test/httputil_test.py +++ b/tornado/test/httputil_test.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function from tornado.httputil import ( url_concat, parse_multipart_form_data, HTTPHeaders, format_timestamp, HTTPServerRequest, parse_request_start_line, parse_cookie, qs_to_qsl, + HTTPInputError, ) from tornado.escape import utf8, native_str from tornado.util import PY3 @@ -283,6 +284,13 @@ Foo: even ("Foo", "bar baz"), ("Foo", "even more lines")]) + def test_malformed_continuation(self): + # If the first line starts with whitespace, it's a + # continuation line with nothing to continue, so reject it + # (with a proper error). + data = " Foo: bar" + self.assertRaises(HTTPInputError, HTTPHeaders.parse, data) + def test_unicode_newlines(self): # Ensure that only \r\n is recognized as a header separator, and not # the other newline-like unicode characters. -- 2.47.2