Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strip trailing optional whitespace (OWS) from single-line header field values #3254

Merged
merged 1 commit into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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""
}
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"",
}
Loading