mirror of
https://github.com/frappe/gunicorn.git
synced 2026-01-14 11:09:11 +08:00
Forbid contradictory secure scheme headers
When a request specifies contradictory secure scheme headers, raise a parse error.
This commit is contained in:
parent
5c92093219
commit
b07532be75
@ -37,11 +37,12 @@ To turn off buffering, you only need to add ``proxy_buffering off;`` to your
|
|||||||
}
|
}
|
||||||
...
|
...
|
||||||
|
|
||||||
When Nginx is handling SSL it is helpful to pass the protocol information
|
It is recommended to pass protocol information to Gunicorn. Many web
|
||||||
to Gunicorn. Many web frameworks use this information to generate URLs.
|
frameworks use this information to generate URLs. Without this
|
||||||
Without this information, the application may mistakenly generate 'http'
|
information, the application may mistakenly generate 'http' URLs in
|
||||||
URLs in 'https' responses, leading to mixed content warnings or broken
|
'https' responses, leading to mixed content warnings or broken
|
||||||
applications. In this case, configure Nginx to pass an appropriate header::
|
applications. To configure Nginx to pass an appropriate header, add
|
||||||
|
a ``proxy_set_header`` directive to your ``location`` block::
|
||||||
|
|
||||||
...
|
...
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|||||||
@ -57,8 +57,7 @@ http {
|
|||||||
|
|
||||||
location @proxy_to_app {
|
location @proxy_to_app {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
# enable this if and only if you use HTTPS
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
# proxy_set_header X-Forwarded-Proto https;
|
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
# we don't want nginx trying to do something clever with
|
# we don't want nginx trying to do something clever with
|
||||||
# redirects, we set the Host: header above already.
|
# redirects, we set the Host: header above already.
|
||||||
|
|||||||
@ -107,3 +107,8 @@ class ForbiddenProxyRequest(ParseException):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Proxy request from %r not allowed" % self.host
|
return "Proxy request from %r not allowed" % self.host
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSchemeHeaders(ParseException):
|
||||||
|
def __str__(self):
|
||||||
|
return "Contradictory scheme headers"
|
||||||
|
|||||||
@ -14,6 +14,7 @@ from gunicorn.http.errors import (InvalidHeader, InvalidHeaderName, NoMoreData,
|
|||||||
InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion,
|
InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion,
|
||||||
LimitRequestLine, LimitRequestHeaders)
|
LimitRequestLine, LimitRequestHeaders)
|
||||||
from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest
|
from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest
|
||||||
|
from gunicorn.http.errors import InvalidSchemeHeaders
|
||||||
from gunicorn.six import BytesIO
|
from gunicorn.six import BytesIO
|
||||||
from gunicorn.util import split_request_uri
|
from gunicorn.util import split_request_uri
|
||||||
|
|
||||||
@ -34,6 +35,7 @@ class Message(object):
|
|||||||
self.headers = []
|
self.headers = []
|
||||||
self.trailers = []
|
self.trailers = []
|
||||||
self.body = None
|
self.body = None
|
||||||
|
self.scheme = "https" if cfg.is_ssl else "http"
|
||||||
|
|
||||||
# set headers limits
|
# set headers limits
|
||||||
self.limit_request_fields = cfg.limit_request_fields
|
self.limit_request_fields = cfg.limit_request_fields
|
||||||
@ -57,11 +59,24 @@ class Message(object):
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def parse_headers(self, data):
|
def parse_headers(self, data):
|
||||||
|
cfg = self.cfg
|
||||||
headers = []
|
headers = []
|
||||||
|
|
||||||
# Split lines on \r\n keeping the \r\n on each line
|
# Split lines on \r\n keeping the \r\n on each line
|
||||||
lines = [bytes_to_str(line) + "\r\n" for line in data.split(b"\r\n")]
|
lines = [bytes_to_str(line) + "\r\n" for line in data.split(b"\r\n")]
|
||||||
|
|
||||||
|
# handle scheme headers
|
||||||
|
scheme_header = False
|
||||||
|
secure_scheme_headers = {}
|
||||||
|
if '*' in cfg.forwarded_allow_ips:
|
||||||
|
secure_scheme_headers = cfg.secure_scheme_headers
|
||||||
|
elif isinstance(self.unreader, SocketUnreader):
|
||||||
|
remote_addr = self.unreader.sock.getpeername()
|
||||||
|
if isinstance(remote_addr, tuple):
|
||||||
|
remote_host = remote_addr[0]
|
||||||
|
if remote_host in cfg.forwarded_allow_ips:
|
||||||
|
secure_scheme_headers = cfg.secure_scheme_headers
|
||||||
|
|
||||||
# Parse headers into key/value pairs paying attention
|
# Parse headers into key/value pairs paying attention
|
||||||
# to continuation lines.
|
# to continuation lines.
|
||||||
while lines:
|
while lines:
|
||||||
@ -92,7 +107,19 @@ class Message(object):
|
|||||||
|
|
||||||
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")
|
||||||
|
|
||||||
|
if name in secure_scheme_headers:
|
||||||
|
secure = value == secure_scheme_headers[name]
|
||||||
|
scheme = "https" if secure else "http"
|
||||||
|
if scheme_header:
|
||||||
|
if scheme != self.scheme:
|
||||||
|
raise InvalidSchemeHeaders()
|
||||||
|
else:
|
||||||
|
scheme_header = True
|
||||||
|
self.scheme = scheme
|
||||||
|
|
||||||
headers.append((name, value))
|
headers.append((name, value))
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
def set_body_reader(self):
|
def set_body_reader(self):
|
||||||
|
|||||||
@ -121,25 +121,14 @@ def create(req, sock, client, server, cfg):
|
|||||||
|
|
||||||
# default variables
|
# default variables
|
||||||
host = None
|
host = None
|
||||||
url_scheme = "https" if cfg.is_ssl else "http"
|
|
||||||
script_name = os.environ.get("SCRIPT_NAME", "")
|
script_name = os.environ.get("SCRIPT_NAME", "")
|
||||||
|
|
||||||
# set secure_headers
|
|
||||||
secure_headers = cfg.secure_scheme_headers
|
|
||||||
if client and not isinstance(client, string_types):
|
|
||||||
if ('*' not in cfg.forwarded_allow_ips
|
|
||||||
and client[0] not in cfg.forwarded_allow_ips):
|
|
||||||
secure_headers = {}
|
|
||||||
|
|
||||||
# add the headers to the environ
|
# add the headers to the environ
|
||||||
for hdr_name, hdr_value in req.headers:
|
for hdr_name, hdr_value in req.headers:
|
||||||
if hdr_name == "EXPECT":
|
if hdr_name == "EXPECT":
|
||||||
# handle expect
|
# handle expect
|
||||||
if hdr_value.lower() == "100-continue":
|
if hdr_value.lower() == "100-continue":
|
||||||
sock.send(b"HTTP/1.1 100 Continue\r\n\r\n")
|
sock.send(b"HTTP/1.1 100 Continue\r\n\r\n")
|
||||||
elif secure_headers and (hdr_name in secure_headers and
|
|
||||||
hdr_value == secure_headers[hdr_name]):
|
|
||||||
url_scheme = "https"
|
|
||||||
elif hdr_name == 'HOST':
|
elif hdr_name == 'HOST':
|
||||||
host = hdr_value
|
host = hdr_value
|
||||||
elif hdr_name == "SCRIPT_NAME":
|
elif hdr_name == "SCRIPT_NAME":
|
||||||
@ -157,7 +146,7 @@ def create(req, sock, client, server, cfg):
|
|||||||
environ[key] = hdr_value
|
environ[key] = hdr_value
|
||||||
|
|
||||||
# set the url scheme
|
# set the url scheme
|
||||||
environ['wsgi.url_scheme'] = url_scheme
|
environ['wsgi.url_scheme'] = req.scheme
|
||||||
|
|
||||||
# set the REMOTE_* keys in environ
|
# set the REMOTE_* keys in environ
|
||||||
# authors should be aware that REMOTE_HOST and REMOTE_ADDR
|
# authors should be aware that REMOTE_HOST and REMOTE_ADDR
|
||||||
@ -182,9 +171,9 @@ def create(req, sock, client, server, cfg):
|
|||||||
if host:
|
if host:
|
||||||
server = host.split(':')
|
server = host.split(':')
|
||||||
if len(server) == 1:
|
if len(server) == 1:
|
||||||
if url_scheme == "http":
|
if req.scheme == "http":
|
||||||
server.append(80)
|
server.append(80)
|
||||||
elif url_scheme == "https":
|
elif req.scheme == "https":
|
||||||
server.append(443)
|
server.append(443)
|
||||||
else:
|
else:
|
||||||
server.append('')
|
server.append('')
|
||||||
|
|||||||
@ -21,6 +21,7 @@ from gunicorn.http.errors import (
|
|||||||
InvalidHTTPVersion, LimitRequestLine, LimitRequestHeaders,
|
InvalidHTTPVersion, LimitRequestLine, LimitRequestHeaders,
|
||||||
)
|
)
|
||||||
from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest
|
from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest
|
||||||
|
from gunicorn.http.errors import InvalidSchemeHeaders
|
||||||
from gunicorn.http.wsgi import default_environ, Response
|
from gunicorn.http.wsgi import default_environ, Response
|
||||||
from gunicorn.six import MAXSIZE
|
from gunicorn.six import MAXSIZE
|
||||||
|
|
||||||
@ -201,6 +202,7 @@ class Worker(object):
|
|||||||
InvalidHTTPVersion, InvalidHeader, InvalidHeaderName,
|
InvalidHTTPVersion, InvalidHeader, InvalidHeaderName,
|
||||||
LimitRequestLine, LimitRequestHeaders,
|
LimitRequestLine, LimitRequestHeaders,
|
||||||
InvalidProxyLine, ForbiddenProxyRequest,
|
InvalidProxyLine, ForbiddenProxyRequest,
|
||||||
|
InvalidSchemeHeaders,
|
||||||
SSLError)):
|
SSLError)):
|
||||||
|
|
||||||
status_int = 400
|
status_int = 400
|
||||||
@ -226,6 +228,8 @@ class Worker(object):
|
|||||||
reason = "Forbidden"
|
reason = "Forbidden"
|
||||||
mesg = "Request forbidden"
|
mesg = "Request forbidden"
|
||||||
status_int = 403
|
status_int = 403
|
||||||
|
elif isinstance(exc, InvalidSchemeHeaders):
|
||||||
|
mesg = "%s" % str(exc)
|
||||||
elif isinstance(exc, SSLError):
|
elif isinstance(exc, SSLError):
|
||||||
reason = "Forbidden"
|
reason = "Forbidden"
|
||||||
mesg = "'%s'" % str(exc)
|
mesg = "'%s'" % str(exc)
|
||||||
|
|||||||
4
tests/requests/invalid/019.http
Normal file
4
tests/requests/invalid/019.http
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
GET /test HTTP/1.1\r\n
|
||||||
|
X-Forwarded-Proto: https\r\n
|
||||||
|
X-Forwarded-Ssl: off\r\n
|
||||||
|
\r\n
|
||||||
6
tests/requests/invalid/019.py
Normal file
6
tests/requests/invalid/019.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from gunicorn.config import Config
|
||||||
|
from gunicorn.http.errors import InvalidSchemeHeaders
|
||||||
|
|
||||||
|
request = InvalidSchemeHeaders
|
||||||
|
cfg = Config()
|
||||||
|
cfg.set('forwarded_allow_ips', '*')
|
||||||
Loading…
x
Reference in New Issue
Block a user