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.
Simply refuse obsolete header folding (a default-off
option to revert is temporarily provided).
While we are at it, explicitly handle recently
introduced http error classes with intended status code.
  • Loading branch information
pajod committed Aug 7, 2024
1 parent 0468272 commit d1094c1
Show file tree
Hide file tree
Showing 14 changed files with 120 additions and 4 deletions.
20 changes: 20 additions & 0 deletions docs/source/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,26 @@ The variables are passed to the PasteDeploy entrypoint. Example::

.. versionadded:: 19.7

.. _permit-obsolete-folding:

``permit_obsolete_folding``
~~~~~~~~~~~~~~~~~~~~~~~~~~~

**Command line:** ``--permit-obsolete-folding``

**Default:** ``False``

Permit requests employing obsolete HTTP line folding mechanism

The folding mechanism was deprecated by rfc7230 Section 3.2.4 and will not be
employed in HTTP request headers from standards-compliant HTTP clients.

This option is provided to diagnose backwards-incompatible changes.
Use with care and only if necessary. Temporary; the precise effect of this option may
change in a future version, or it may be removed altogether.

.. versionadded:: 23.0.0

.. _strip-header-spaces:

``strip_header_spaces``
Expand Down
21 changes: 21 additions & 0 deletions gunicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2243,6 +2243,27 @@ class PasteGlobalConf(Setting):
"""


class PermitObsoleteFolding(Setting):
name = "permit_obsolete_folding"
section = "Server Mechanics"
cli = ["--permit-obsolete-folding"]
validator = validate_bool
action = "store_true"
default = False
desc = """\
Permit requests employing obsolete HTTP line folding mechanism
The folding mechanism was deprecated by rfc7230 Section 3.2.4 and will not be
employed in HTTP request headers from standards-compliant HTTP clients.
This option is provided to diagnose backwards-incompatible changes.
Use with care and only if necessary. Temporary; the precise effect of this option may
change in a future version, or it may be removed altogether.
.. versionadded:: 23.0.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
9 changes: 6 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 @@ -110,10 +110,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
# Consume value continuation lines..
while lines and lines[0].startswith((" ", "\t")):
# .. which is obsolete here, and no longer done by default
if not self.cfg.permit_obsolete_folding:
raise ObsoleteFolding(name)
curr = lines.pop(0)
header_length += len(curr) + len("\r\n")
if header_length > self.limit_request_field_size > 0:
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
4 changes: 4 additions & 0 deletions tests/requests/invalid/013.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@
request = LimitRequestHeaders
cfg = Config()
cfg.set('limit_request_field_size', 14)

# once this option is removed, this test should not be dropped;
# rather, add something involving unnessessary padding
cfg.set('permit_obsolete_folding', True)
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
3 changes: 3 additions & 0 deletions tests/requests/invalid/obs_fold_01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from gunicorn.http.errors import ObsoleteFolding

request = ObsoleteFolding
5 changes: 5 additions & 0 deletions tests/requests/valid/compat_obs_fold.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
16 changes: 16 additions & 0 deletions tests/requests/valid/compat_obs_fold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from gunicorn.http.errors import ObsoleteFolding
from gunicorn.config import Config

cfg = Config()
cfg.set('permit_obsolete_folding', True)

request = {
"method": "GET",
"uri": uri("/"),
"version": (1, 1),
"headers": [
("LONG", "one two"),
("HOST", "localhost"),
],
"body": b""
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from gunicorn.config import Config

cfg = Config()
cfg.set('permit_obsolete_folding', True)

certificate = """-----BEGIN CERTIFICATE-----
MIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx
ETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT
Expand Down
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 d1094c1

Please sign in to comment.