mirror of
https://github.com/frappe/gunicorn.git
synced 2026-01-14 11:09:11 +08:00
Merge pull request #3260 from benoitc/fix-te
don't tolerate wrong te headers
This commit is contained in:
commit
ff2109e759
@ -2369,30 +2369,3 @@ class HeaderMap(Setting):
|
|||||||
|
|
||||||
.. versionadded:: 22.0.0
|
.. versionadded:: 22.0.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class TolerateDangerousFraming(Setting):
|
|
||||||
name = "tolerate_dangerous_framing"
|
|
||||||
section = "Server Mechanics"
|
|
||||||
cli = ["--tolerate-dangerous-framing"]
|
|
||||||
validator = validate_bool
|
|
||||||
action = "store_true"
|
|
||||||
default = False
|
|
||||||
desc = """\
|
|
||||||
Process requests with both Transfer-Encoding and Content-Length
|
|
||||||
|
|
||||||
This is known to induce vulnerabilities, but not strictly forbidden by RFC9112.
|
|
||||||
|
|
||||||
In any case, the connection is closed after the malformed request,
|
|
||||||
as it is unclear if and at which boundary additional requests start.
|
|
||||||
|
|
||||||
Use with care and only if necessary.
|
|
||||||
Temporary; will be changed or removed in a future version.
|
|
||||||
|
|
||||||
.. versionadded:: 22.0.0
|
|
||||||
.. versionchanged: 22.1.0
|
|
||||||
The newly added rejection of invalid and dangerous characters CR, LF and NUL in
|
|
||||||
header field values is also controlled with this setting. rfc9110 permits both
|
|
||||||
rejecting and SP-replacing. With this option set, Gunicorn passes the field value
|
|
||||||
unchanged. With this option unset, Gunicorn rejects the request.
|
|
||||||
"""
|
|
||||||
|
|||||||
@ -123,10 +123,7 @@ class Message(object):
|
|||||||
value = " ".join(value)
|
value = " ".join(value)
|
||||||
|
|
||||||
if RFC9110_5_5_INVALID_AND_DANGEROUS.search(value):
|
if RFC9110_5_5_INVALID_AND_DANGEROUS.search(value):
|
||||||
if not self.cfg.tolerate_dangerous_framing:
|
|
||||||
raise InvalidHeader(name)
|
raise InvalidHeader(name)
|
||||||
# value = RFC9110_5_5_INVALID_AND_DANGEROUS.sub(" ", value)
|
|
||||||
self.force_close()
|
|
||||||
|
|
||||||
if header_length > self.limit_request_field_size > 0:
|
if header_length > self.limit_request_field_size > 0:
|
||||||
raise LimitRequestHeaders("limit request headers fields size")
|
raise LimitRequestHeaders("limit request headers fields size")
|
||||||
@ -172,30 +169,26 @@ class Message(object):
|
|||||||
raise InvalidHeader("CONTENT-LENGTH", req=self)
|
raise InvalidHeader("CONTENT-LENGTH", req=self)
|
||||||
content_length = value
|
content_length = value
|
||||||
elif name == "TRANSFER-ENCODING":
|
elif name == "TRANSFER-ENCODING":
|
||||||
if value.lower() == "chunked":
|
# T-E can be a list
|
||||||
|
# https://datatracker.ietf.org/doc/html/rfc9112#name-transfer-encoding
|
||||||
|
vals = [v.strip() for v in value.split(',')]
|
||||||
|
for val in vals:
|
||||||
|
if val.lower() == "chunked":
|
||||||
# DANGER: transfer codings stack, and stacked chunking is never intended
|
# DANGER: transfer codings stack, and stacked chunking is never intended
|
||||||
if chunked:
|
if chunked:
|
||||||
raise InvalidHeader("TRANSFER-ENCODING", req=self)
|
raise InvalidHeader("TRANSFER-ENCODING", req=self)
|
||||||
chunked = True
|
chunked = True
|
||||||
elif value.lower() == "identity":
|
elif val.lower() == "identity":
|
||||||
# does not do much, could still plausibly desync from what the proxy does
|
# does not do much, could still plausibly desync from what the proxy does
|
||||||
# safe option: nuke it, its never needed
|
# safe option: nuke it, its never needed
|
||||||
if chunked:
|
if chunked:
|
||||||
raise InvalidHeader("TRANSFER-ENCODING", req=self)
|
raise InvalidHeader("TRANSFER-ENCODING", req=self)
|
||||||
elif value.lower() == "":
|
elif val.lower() in ('compress', 'deflate', 'gzip'):
|
||||||
# lacking security review on this case
|
# chunked should be the last one
|
||||||
# offer the option to restore previous behaviour, but refuse by default, for now
|
if chunked:
|
||||||
|
raise InvalidHeader("TRANSFER-ENCODING", req=self)
|
||||||
self.force_close()
|
self.force_close()
|
||||||
if not self.cfg.tolerate_dangerous_framing:
|
|
||||||
raise UnsupportedTransferCoding(value)
|
|
||||||
# DANGER: do not change lightly; ref: request smuggling
|
|
||||||
# T-E is a list and we *could* support correctly parsing its elements
|
|
||||||
# .. but that is only safe after getting all the edge cases right
|
|
||||||
# .. for which no real-world need exists, so best to NOT open that can of worms
|
|
||||||
else:
|
else:
|
||||||
self.force_close()
|
|
||||||
# even if parser is extended, retain this branch:
|
|
||||||
# the "chunked not last" case remains to be rejected!
|
|
||||||
raise UnsupportedTransferCoding(value)
|
raise UnsupportedTransferCoding(value)
|
||||||
|
|
||||||
if chunked:
|
if chunked:
|
||||||
@ -204,15 +197,10 @@ class Message(object):
|
|||||||
# b) chunked HTTP/1.0 (always faulty)
|
# b) chunked HTTP/1.0 (always faulty)
|
||||||
if self.version < (1, 1):
|
if self.version < (1, 1):
|
||||||
# framing wonky, see RFC 9112 Section 6.1
|
# framing wonky, see RFC 9112 Section 6.1
|
||||||
self.force_close()
|
|
||||||
if not self.cfg.tolerate_dangerous_framing:
|
|
||||||
raise InvalidHeader("TRANSFER-ENCODING", req=self)
|
raise InvalidHeader("TRANSFER-ENCODING", req=self)
|
||||||
if content_length is not None:
|
if content_length is not None:
|
||||||
# we cannot be certain the message framing we understood matches proxy intent
|
# we cannot be certain the message framing we understood matches proxy intent
|
||||||
# -> whatever happens next, remaining input must not be trusted
|
# -> whatever happens next, remaining input must not be trusted
|
||||||
self.force_close()
|
|
||||||
# either processing or rejecting is permitted in RFC 9112 Section 6.1
|
|
||||||
if not self.cfg.tolerate_dangerous_framing:
|
|
||||||
raise InvalidHeader("CONTENT-LENGTH", req=self)
|
raise InvalidHeader("CONTENT-LENGTH", req=self)
|
||||||
self.body = Body(ChunkedReader(self, self.unreader))
|
self.body = Body(ChunkedReader(self, self.unreader))
|
||||||
elif content_length is not None:
|
elif content_length is not None:
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
from gunicorn.http.errors import UnsupportedTransferCoding
|
from gunicorn.http.errors import InvalidHeader
|
||||||
request = UnsupportedTransferCoding
|
request = InvalidHeader
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
from gunicorn.http.errors import UnsupportedTransferCoding
|
from gunicorn.http.errors import InvalidHeader
|
||||||
request = UnsupportedTransferCoding
|
request = InvalidHeader
|
||||||
|
|||||||
@ -1,24 +1,10 @@
|
|||||||
POST /chunked_cont_h_at_first HTTP/1.1\r\n
|
POST /chunked HTTP/1.1\r\n
|
||||||
|
Transfer-Encoding: gzip\r\n
|
||||||
Transfer-Encoding: chunked\r\n
|
Transfer-Encoding: chunked\r\n
|
||||||
\r\n
|
\r\n
|
||||||
5; some; parameters=stuff\r\n
|
5\r\n
|
||||||
hello\r\n
|
hello\r\n
|
||||||
6 \t;\tblahblah; blah\r\n
|
6\r\n
|
||||||
world\r\n
|
world\r\n
|
||||||
0\r\n
|
0\r\n
|
||||||
\r\n
|
\r\n
|
||||||
PUT /chunked_cont_h_at_last HTTP/1.1\r\n
|
|
||||||
Transfer-Encoding: chunked\r\n
|
|
||||||
Content-Length: -1\r\n
|
|
||||||
\r\n
|
|
||||||
5; some; parameters=stuff\r\n
|
|
||||||
hello\r\n
|
|
||||||
6; blahblah; blah\r\n
|
|
||||||
world\r\n
|
|
||||||
0\r\n
|
|
||||||
\r\n
|
|
||||||
PUT /ignored_after_dangerous_framing HTTP/1.1\r\n
|
|
||||||
Content-Length: 3\r\n
|
|
||||||
\r\n
|
|
||||||
foo\r\n
|
|
||||||
\r\n
|
|
||||||
|
|||||||
@ -1,27 +1,10 @@
|
|||||||
from gunicorn.config import Config
|
request = {
|
||||||
|
|
||||||
cfg = Config()
|
|
||||||
cfg.set("tolerate_dangerous_framing", True)
|
|
||||||
|
|
||||||
req1 = {
|
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"uri": uri("/chunked_cont_h_at_first"),
|
"uri": uri("/chunked"),
|
||||||
"version": (1, 1),
|
"version": (1, 1),
|
||||||
"headers": [
|
"headers": [
|
||||||
("TRANSFER-ENCODING", "chunked")
|
('TRANSFER-ENCODING', 'gzip'),
|
||||||
|
('TRANSFER-ENCODING', 'chunked')
|
||||||
],
|
],
|
||||||
"body": b"hello world"
|
"body": b"hello world"
|
||||||
}
|
}
|
||||||
|
|
||||||
req2 = {
|
|
||||||
"method": "PUT",
|
|
||||||
"uri": uri("/chunked_cont_h_at_last"),
|
|
||||||
"version": (1, 1),
|
|
||||||
"headers": [
|
|
||||||
("TRANSFER-ENCODING", "chunked"),
|
|
||||||
("CONTENT-LENGTH", "-1"),
|
|
||||||
],
|
|
||||||
"body": b"hello world"
|
|
||||||
}
|
|
||||||
|
|
||||||
request = [req1, req2]
|
|
||||||
|
|||||||
9
tests/requests/valid/025_line.http
Normal file
9
tests/requests/valid/025_line.http
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
POST /chunked HTTP/1.1\r\n
|
||||||
|
Transfer-Encoding: gzip,chunked\r\n
|
||||||
|
\r\n
|
||||||
|
5\r\n
|
||||||
|
hello\r\n
|
||||||
|
6\r\n
|
||||||
|
world\r\n
|
||||||
|
0\r\n
|
||||||
|
\r\n
|
||||||
10
tests/requests/valid/025_line.py
Normal file
10
tests/requests/valid/025_line.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
request = {
|
||||||
|
"method": "POST",
|
||||||
|
"uri": uri("/chunked"),
|
||||||
|
"version": (1, 1),
|
||||||
|
"headers": [
|
||||||
|
('TRANSFER-ENCODING', 'gzip,chunked')
|
||||||
|
|
||||||
|
],
|
||||||
|
"body": b"hello world"
|
||||||
|
}
|
||||||
@ -1,18 +0,0 @@
|
|||||||
POST /chunked_cont_h_at_first HTTP/1.1\r\n
|
|
||||||
Transfer-Encoding: chunked\r\n
|
|
||||||
\r\n
|
|
||||||
5; some; parameters=stuff\r\n
|
|
||||||
hello\r\n
|
|
||||||
6; blahblah; blah\r\n
|
|
||||||
world\r\n
|
|
||||||
0\r\n
|
|
||||||
\r\n
|
|
||||||
PUT /chunked_cont_h_at_last HTTP/1.1\r\n
|
|
||||||
Transfer-Encoding: chunked\r\n
|
|
||||||
Content-Length: -1\r\n
|
|
||||||
\r\n
|
|
||||||
5; some; parameters=stuff\r\n
|
|
||||||
hello\r\n
|
|
||||||
6; blahblah; blah\r\n
|
|
||||||
world\r\n
|
|
||||||
0\r\n
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
from gunicorn.config import Config
|
|
||||||
|
|
||||||
cfg = Config()
|
|
||||||
cfg.set("tolerate_dangerous_framing", True)
|
|
||||||
|
|
||||||
req1 = {
|
|
||||||
"method": "POST",
|
|
||||||
"uri": uri("/chunked_cont_h_at_first"),
|
|
||||||
"version": (1, 1),
|
|
||||||
"headers": [
|
|
||||||
("TRANSFER-ENCODING", "chunked")
|
|
||||||
],
|
|
||||||
"body": b"hello world"
|
|
||||||
}
|
|
||||||
|
|
||||||
req2 = {
|
|
||||||
"method": "PUT",
|
|
||||||
"uri": uri("/chunked_cont_h_at_last"),
|
|
||||||
"version": (1, 1),
|
|
||||||
"headers": [
|
|
||||||
("TRANSFER-ENCODING", "chunked"),
|
|
||||||
("CONTENT-LENGTH", "-1"),
|
|
||||||
],
|
|
||||||
"body": b"hello world"
|
|
||||||
}
|
|
||||||
|
|
||||||
request = [req1, req2]
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
GET / HTTP/1.1\r\n
|
|
||||||
Host: x\r\n
|
|
||||||
Newline: a\n
|
|
||||||
Content-Length: 26\r\n
|
|
||||||
X-Forwarded-By: broken-proxy\r\n\r\n
|
|
||||||
GET / HTTP/1.1\n
|
|
||||||
Host: x\r\n
|
|
||||||
\r\n
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
from gunicorn.config import Config
|
|
||||||
|
|
||||||
cfg = Config()
|
|
||||||
cfg.set("tolerate_dangerous_framing", True)
|
|
||||||
|
|
||||||
req1 = {
|
|
||||||
"method": "GET",
|
|
||||||
"uri": uri("/"),
|
|
||||||
"version": (1, 1),
|
|
||||||
"headers": [
|
|
||||||
("HOST", "x"),
|
|
||||||
("NEWLINE", "a\nContent-Length: 26"),
|
|
||||||
("X-FORWARDED-BY", "broken-proxy"),
|
|
||||||
],
|
|
||||||
"body": b""
|
|
||||||
}
|
|
||||||
|
|
||||||
request = [req1]
|
|
||||||
Loading…
x
Reference in New Issue
Block a user