Merge pull request #3463 from pajod/patch-100-continue

Restrict 100 Continue resonses to reasonable count (1) & situation (HTTP/1.1)
This commit is contained in:
Benoit Chesneau 2026-01-25 09:37:49 +01:00 committed by GitHub
commit 98156f9ef6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 54 additions and 7 deletions

View File

@ -15,6 +15,7 @@ import socket
import struct
from gunicorn.http.errors import (
ExpectationFailed,
InvalidHeader, InvalidHeaderName, NoMoreData,
InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion,
LimitRequestLine, LimitRequestHeaders,
@ -83,6 +84,7 @@ class AsyncRequest:
self.trailers = []
self.scheme = "https" if cfg.is_ssl else "http"
self.must_close = False
self._expected_100_continue = False
self.proxy_protocol_info = None
@ -474,6 +476,21 @@ class AsyncRequest:
if header_length > self.limit_request_field_size > 0:
raise LimitRequestHeaders("limit request headers fields size")
if not from_trailer and name == "EXPECT":
# https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1
# "The Expect field value is case-insensitive."
if value.lower() == "100-continue":
if self.version < (1, 1):
# https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1-12
# "A server that receives a 100-continue expectation
# in an HTTP/1.0 request MUST ignore that expectation."
pass
else:
self._expected_100_continue = True
# N.B. understood but ignored expect header does not return 417
else:
raise ExpectationFailed(value)
if name in secure_scheme_headers:
secure = value == secure_scheme_headers[name]
scheme = "https" if secure else "http"

View File

@ -47,6 +47,14 @@ class InvalidRequestMethod(ParseException):
return "Invalid HTTP method: %r" % self.method
class ExpectationFailed(ParseException):
def __init__(self, expect):
self.expect = expect
def __str__(self):
return "Unable to comply with expectation: %r" % (self.expect, )
class InvalidHTTPVersion(ParseException):
def __init__(self, version):
self.version = version

View File

@ -14,6 +14,7 @@ from gunicorn.http.errors import (
InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion,
LimitRequestLine, LimitRequestHeaders,
UnsupportedTransferCoding, ObsoleteFolding,
ExpectationFailed,
)
from gunicorn.http.errors import InvalidProxyLine, InvalidProxyHeader, ForbiddenProxyRequest
from gunicorn.http.errors import InvalidSchemeHeaders
@ -90,6 +91,7 @@ class Message:
self.body = None
self.scheme = "https" if cfg.is_ssl else "http"
self.must_close = False
self._expected_100_continue = False
# set headers limits
self.limit_request_fields = cfg.limit_request_fields
@ -180,6 +182,21 @@ class Message:
if header_length > self.limit_request_field_size > 0:
raise LimitRequestHeaders("limit request headers fields size")
if not from_trailer and name == "EXPECT":
# https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1
# "The Expect field value is case-insensitive."
if value.lower() == "100-continue":
if self.version < (1, 1):
# https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1-12
# "A server that receives a 100-continue expectation
# in an HTTP/1.0 request MUST ignore that expectation."
pass
else:
self._expected_100_continue = True
# N.B. understood but ignored expect header does not return 417
else:
raise ExpectationFailed(value)
if name in secure_scheme_headers:
secure = value == secure_scheme_headers[name]
scheme = "https" if secure else "http"

View File

@ -117,13 +117,14 @@ def create(req, sock, client, server, cfg):
host = None
script_name = os.environ.get("SCRIPT_NAME", "")
if req._expected_100_continue:
sock.send(b"HTTP/1.1 100 Continue\r\n\r\n")
# rfc9112: Expect MUST be forwarded if the request is forwarded
# N.B. gunicorn just sends at most one - application might send another
# add the headers to the environ
for hdr_name, hdr_value in req.headers:
if hdr_name == "EXPECT":
# handle expect
if hdr_value.lower() == "100-continue":
sock.send(b"HTTP/1.1 100 Continue\r\n\r\n")
elif hdr_name == 'HOST':
if hdr_name == 'HOST':
host = hdr_value
elif hdr_name == "SCRIPT_NAME":
script_name = hdr_value

View File

@ -19,7 +19,7 @@ from gunicorn.http.errors import (
InvalidProxyLine, InvalidRequestLine,
InvalidRequestMethod, InvalidSchemeHeaders,
LimitRequestHeaders, LimitRequestLine,
UnsupportedTransferCoding,
UnsupportedTransferCoding, ExpectationFailed,
ConfigurationProblem, ObsoleteFolding,
)
from gunicorn.http.wsgi import Response, default_environ
@ -212,7 +212,7 @@ class Worker:
LimitRequestLine, LimitRequestHeaders,
InvalidProxyLine, ForbiddenProxyRequest,
InvalidSchemeHeaders, UnsupportedTransferCoding,
ConfigurationProblem, ObsoleteFolding,
ConfigurationProblem, ObsoleteFolding, ExpectationFailed,
SSLError,
)):
@ -239,6 +239,10 @@ class Worker:
req = exc.req # for access log
elif isinstance(exc, LimitRequestLine):
mesg = "%s" % str(exc)
elif isinstance(exc, ExpectationFailed):
reason = "Expectation Failed"
mesg = str(exc)
status_int = 417
elif isinstance(exc, LimitRequestHeaders):
reason = "Request Header Fields Too Large"
mesg = "Error parsing headers: '%s'" % str(exc)