Skip to content

Commit

Permalink
whitespace handling in header field values
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pajod committed Jul 31, 2024
1 parent e3fa50d commit 8fdb839
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 4 deletions.
17 changes: 17 additions & 0 deletions docs/source/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand Down
18 changes: 18 additions & 0 deletions gunicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions gunicorn/http/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions gunicorn/http/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
13 changes: 12 additions & 1 deletion gunicorn/workers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)):

Expand All @@ -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"):
Expand Down
5 changes: 5 additions & 0 deletions tests/requests/invalid/obs_fold_01.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
GET / HTTP/1.1\r\n
Long: one\r\n
two\r\n
Host: localhost\r\n
\r\n
7 changes: 7 additions & 0 deletions tests/requests/invalid/obs_fold_01.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions tests/requests/valid/padding_01.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
GET / HTTP/1.1\r\n
Host: localhost\r\n
Name: \t value \t \r\n
\r\n
11 changes: 11 additions & 0 deletions tests/requests/valid/padding_01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

request = {
"method": "GET",
"uri": uri("/"),
"version": (1, 1),
"headers": [
("HOST", "localhost"),
("NAME", "value")
],
"body": b"",
}

0 comments on commit 8fdb839

Please sign in to comment.