From 8fdb839ef9aa239d71c065e628bc00b13cba28ca Mon Sep 17 00:00:00 2001 From: "Paul J. Dorn" Date: Wed, 31 Jul 2024 01:54:41 +0200 Subject: [PATCH] whitespace handling in header field values Strip whitespace also *after* header field value. Intoduce a default-off option to simply refuse obsolete header folding. While we are at it, explicitly handle recently introduced http error classes with intended status code. --- docs/source/settings.rst | 17 +++++++++++++++++ gunicorn/config.py | 18 ++++++++++++++++++ gunicorn/http/errors.py | 8 ++++++++ gunicorn/http/message.py | 10 +++++++--- gunicorn/workers/base.py | 13 ++++++++++++- tests/requests/invalid/obs_fold_01.http | 5 +++++ tests/requests/invalid/obs_fold_01.py | 7 +++++++ tests/requests/valid/padding_01.http | 4 ++++ tests/requests/valid/padding_01.py | 11 +++++++++++ 9 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 tests/requests/invalid/obs_fold_01.http create mode 100644 tests/requests/invalid/obs_fold_01.py create mode 100644 tests/requests/valid/padding_01.http create mode 100644 tests/requests/valid/padding_01.py diff --git a/docs/source/settings.rst b/docs/source/settings.rst index a5bd51c58..0438aaa04 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -1396,6 +1396,23 @@ The variables are passed to the PasteDeploy entrypoint. Example:: .. versionadded:: 19.7 +.. _refuse-obsolete-folding: + +``refuse_obsolete_folding`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Command line:** ``--refuse-obsolete-folding`` + +**Default:** ``False`` + +Refuse requests employing obsolete HTTP line folding mechanism + +The mechanism was deprecated by rfc7230 Section 3.2.4. + +Safe to enable if you only ever want to serve standards compliant HTTP clients. + +.. versionadded:: 22.1.0 + .. _strip-header-spaces: ``strip_header_spaces`` diff --git a/gunicorn/config.py b/gunicorn/config.py index 144acaecc..b38a115a1 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -2241,6 +2241,24 @@ class PasteGlobalConf(Setting): """ +class RefuseObsoleteFolding(Setting): + name = "refuse_obsolete_folding" + section = "Server Mechanics" + cli = ["--refuse-obsolete-folding"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Refuse requests employing obsolete HTTP line folding mechanism + + The mechanism was deprecated by rfc7230 Section 3.2.4. + + Safe to enable if you only ever want to serve standards compliant HTTP clients. + + .. versionadded:: 22.1.0 + """ + + class StripHeaderSpaces(Setting): name = "strip_header_spaces" section = "Server Mechanics" diff --git a/gunicorn/http/errors.py b/gunicorn/http/errors.py index 1e3c5e752..861dcb4f8 100644 --- a/gunicorn/http/errors.py +++ b/gunicorn/http/errors.py @@ -65,6 +65,14 @@ def __str__(self): return "Invalid HTTP Header: %r" % self.hdr +class ObsoleteFolding(ParseException): + def __init__(self, hdr): + self.hdr = hdr + + def __str__(self): + return "Obsolete line folding is unacceptable: %r" % (self.hdr, ) + + class InvalidHeaderName(ParseException): def __init__(self, hdr): self.hdr = hdr diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py index 88ffa5a25..83ff640ac 100644 --- a/gunicorn/http/message.py +++ b/gunicorn/http/message.py @@ -12,7 +12,7 @@ InvalidHeader, InvalidHeaderName, NoMoreData, InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion, LimitRequestLine, LimitRequestHeaders, - UnsupportedTransferCoding, + UnsupportedTransferCoding, ObsoleteFolding, ) from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest from gunicorn.http.errors import InvalidSchemeHeaders @@ -109,9 +109,13 @@ def parse_headers(self, data, from_trailer=False): # b"\xDF".decode("latin-1").upper().encode("ascii") == b"SS" name = name.upper() - value = [value.lstrip(" \t")] + value = [value.strip(" \t")] - # Consume value continuation lines + # Refuse obsolete folding + if self.cfg.refuse_obsolete_folding: + if lines and lines[0].startswith((" ", "\t")): + raise ObsoleteFolding(name) + # OR: Consume value continuation lines while lines and lines[0].startswith((" ", "\t")): curr = lines.pop(0) header_length += len(curr) + len("\r\n") diff --git a/gunicorn/workers/base.py b/gunicorn/workers/base.py index f97d923c7..bf0ce32ca 100644 --- a/gunicorn/workers/base.py +++ b/gunicorn/workers/base.py @@ -20,6 +20,8 @@ InvalidProxyLine, InvalidRequestLine, InvalidRequestMethod, InvalidSchemeHeaders, LimitRequestHeaders, LimitRequestLine, + UnsupportedTransferCoding, + ConfigurationProblem, ObsoleteFolding, ) from gunicorn.http.wsgi import Response, default_environ from gunicorn.reloader import reloader_engines @@ -210,7 +212,8 @@ def handle_error(self, req, client, addr, exc): InvalidHTTPVersion, InvalidHeader, InvalidHeaderName, LimitRequestLine, LimitRequestHeaders, InvalidProxyLine, ForbiddenProxyRequest, - InvalidSchemeHeaders, + InvalidSchemeHeaders, UnsupportedTransferCoding, + ConfigurationProblem, ObsoleteFolding, SSLError, )): @@ -223,6 +226,14 @@ def handle_error(self, req, client, addr, exc): mesg = "Invalid Method '%s'" % str(exc) elif isinstance(exc, InvalidHTTPVersion): mesg = "Invalid HTTP Version '%s'" % str(exc) + elif isinstance(exc, UnsupportedTransferCoding): + mesg = "%s" % str(exc) + status_int = 501 + elif isinstance(exc, ConfigurationProblem): + mesg = "%s" % str(exc) + status_int = 500 + elif isinstance(exc, ObsoleteFolding): + mesg = "%s" % str(exc) elif isinstance(exc, (InvalidHeaderName, InvalidHeader,)): mesg = "%s" % str(exc) if not req and hasattr(exc, "req"): diff --git a/tests/requests/invalid/obs_fold_01.http b/tests/requests/invalid/obs_fold_01.http new file mode 100644 index 000000000..d23b41750 --- /dev/null +++ b/tests/requests/invalid/obs_fold_01.http @@ -0,0 +1,5 @@ +GET / HTTP/1.1\r\n +Long: one\r\n + two\r\n +Host: localhost\r\n +\r\n diff --git a/tests/requests/invalid/obs_fold_01.py b/tests/requests/invalid/obs_fold_01.py new file mode 100644 index 000000000..1be104e7b --- /dev/null +++ b/tests/requests/invalid/obs_fold_01.py @@ -0,0 +1,7 @@ +from gunicorn.http.errors import ObsoleteFolding +from gunicorn.config import Config + +cfg = Config() +cfg.set('refuse_obsolete_folding', True) + +request = ObsoleteFolding diff --git a/tests/requests/valid/padding_01.http b/tests/requests/valid/padding_01.http new file mode 100644 index 000000000..757208b08 --- /dev/null +++ b/tests/requests/valid/padding_01.http @@ -0,0 +1,4 @@ +GET / HTTP/1.1\r\n +Host: localhost\r\n +Name: \t value \t \r\n +\r\n diff --git a/tests/requests/valid/padding_01.py b/tests/requests/valid/padding_01.py new file mode 100644 index 000000000..d58f67207 --- /dev/null +++ b/tests/requests/valid/padding_01.py @@ -0,0 +1,11 @@ + +request = { + "method": "GET", + "uri": uri("/"), + "version": (1, 1), + "headers": [ + ("HOST", "localhost"), + ("NAME", "value") + ], + "body": b"", +}