Forbid contradictory secure scheme headers

When a request specifies contradictory secure scheme headers, raise a
parse error.
This commit is contained in:
Randall Leeds 2018-01-10 11:44:56 -08:00
parent 5c92093219
commit b07532be75
8 changed files with 56 additions and 21 deletions

View File

@ -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;

View File

@ -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.

View File

@ -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"

View File

@ -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):

View File

@ -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('')

View File

@ -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)

View 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

View 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', '*')