Skip to content

Commit

Permalink
Forbid contradictory secure scheme headers
Browse files Browse the repository at this point in the history
When a request specifies contradictory secure scheme headers, raise a
parse error.
  • Loading branch information
tilgovi committed Jan 10, 2018
1 parent 5c92093 commit b07532b
Show file tree
Hide file tree
Showing 8 changed files with 56 additions and 21 deletions.
11 changes: 6 additions & 5 deletions docs/source/deploy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
to Gunicorn. Many web frameworks use this information to generate URLs.
Without this information, the application may mistakenly generate 'http'
URLs in 'https' responses, leading to mixed content warnings or broken
applications. In this case, configure Nginx to pass an appropriate header::
It is recommended to pass protocol information to Gunicorn. Many web
frameworks use this information to generate URLs. Without this
information, the application may mistakenly generate 'http' URLs in
'https' responses, leading to mixed content warnings or broken
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;
Expand Down
3 changes: 1 addition & 2 deletions examples/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ http {

location @proxy_to_app {
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 https;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
Expand Down
5 changes: 5 additions & 0 deletions gunicorn/http/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,8 @@ def __init__(self, host):

def __str__(self):
return "Proxy request from %r not allowed" % self.host


class InvalidSchemeHeaders(ParseException):
def __str__(self):
return "Contradictory scheme headers"
27 changes: 27 additions & 0 deletions gunicorn/http/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion,
LimitRequestLine, LimitRequestHeaders)
from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest
from gunicorn.http.errors import InvalidSchemeHeaders
from gunicorn.six import BytesIO
from gunicorn.util import split_request_uri

Expand All @@ -34,6 +35,7 @@ def __init__(self, cfg, unreader):
self.headers = []
self.trailers = []
self.body = None
self.scheme = "https" if cfg.is_ssl else "http"

# set headers limits
self.limit_request_fields = cfg.limit_request_fields
Expand All @@ -57,11 +59,24 @@ def parse(self, unreader):
raise NotImplementedError()

def parse_headers(self, data):
cfg = self.cfg
headers = []

# 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")]

# 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
# to continuation lines.
while lines:
Expand Down Expand Up @@ -92,7 +107,19 @@ def parse_headers(self, data):

if header_length > self.limit_request_field_size > 0:
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))

return headers

def set_body_reader(self):
Expand Down
17 changes: 3 additions & 14 deletions gunicorn/http/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,25 +121,14 @@ def create(req, sock, client, server, cfg):

# default variables
host = None
url_scheme = "https" if cfg.is_ssl else "http"
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
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 secure_headers and (hdr_name in secure_headers and
hdr_value == secure_headers[hdr_name]):
url_scheme = "https"
elif hdr_name == 'HOST':
host = hdr_value
elif hdr_name == "SCRIPT_NAME":
Expand All @@ -157,7 +146,7 @@ def create(req, sock, client, server, cfg):
environ[key] = hdr_value

# set the url scheme
environ['wsgi.url_scheme'] = url_scheme
environ['wsgi.url_scheme'] = req.scheme

# set the REMOTE_* keys in environ
# authors should be aware that REMOTE_HOST and REMOTE_ADDR
Expand All @@ -182,9 +171,9 @@ def create(req, sock, client, server, cfg):
if host:
server = host.split(':')
if len(server) == 1:
if url_scheme == "http":
if req.scheme == "http":
server.append(80)
elif url_scheme == "https":
elif req.scheme == "https":
server.append(443)
else:
server.append('')
Expand Down
4 changes: 4 additions & 0 deletions gunicorn/workers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
InvalidHTTPVersion, LimitRequestLine, LimitRequestHeaders,
)
from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest
from gunicorn.http.errors import InvalidSchemeHeaders
from gunicorn.http.wsgi import default_environ, Response
from gunicorn.six import MAXSIZE

Expand Down Expand Up @@ -201,6 +202,7 @@ def handle_error(self, req, client, addr, exc):
InvalidHTTPVersion, InvalidHeader, InvalidHeaderName,
LimitRequestLine, LimitRequestHeaders,
InvalidProxyLine, ForbiddenProxyRequest,
InvalidSchemeHeaders,
SSLError)):

status_int = 400
Expand All @@ -226,6 +228,8 @@ def handle_error(self, req, client, addr, exc):
reason = "Forbidden"
mesg = "Request forbidden"
status_int = 403
elif isinstance(exc, InvalidSchemeHeaders):
mesg = "%s" % str(exc)
elif isinstance(exc, SSLError):
reason = "Forbidden"
mesg = "'%s'" % str(exc)
Expand Down
4 changes: 4 additions & 0 deletions tests/requests/invalid/019.http
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions tests/requests/invalid/019.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from gunicorn.config import Config
from gunicorn.http.errors import InvalidSchemeHeaders

request = InvalidSchemeHeaders
cfg = Config()
cfg.set('forwarded_allow_ips', '*')

0 comments on commit b07532b

Please sign in to comment.