Skip to content

Commit

Permalink
Configurable list of forwarder headers
Browse files Browse the repository at this point in the history
  • Loading branch information
pajod committed Apr 25, 2024
1 parent 8d93ff8 commit debe5bb
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 11 deletions.
44 changes: 40 additions & 4 deletions gunicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import copy
import grp
import inspect
import ipaddress
import os
import pwd
import re
Expand Down Expand Up @@ -400,6 +401,17 @@ def validate_list_of_existing_files(val):
return [validate_file_exists(v) for v in validate_list_string(val)]


def validate_string_to_addr_list(val):
val = validate_string_to_list(val)

for addr in val:
if addr == "*":
continue
_vaid_ip = ipaddress.ip_address(addr)

return val


def validate_string_to_list(val):
val = validate_string(val)

Expand Down Expand Up @@ -1260,7 +1272,7 @@ class ForwardedAllowIPS(Setting):
section = "Server Mechanics"
cli = ["--forwarded-allow-ips"]
meta = "STRING"
validator = validate_string_to_list
validator = validate_string_to_addr_list
default = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1,::1")
desc = """\
Front-end's IPs from which allowed to handle set secure headers.
Expand Down Expand Up @@ -2340,6 +2352,29 @@ def validate_header_map_behaviour(val):
else:
raise ValueError("Invalid header map behaviour: %s" % val)

class ForwarderHeaders(Setting):
name = "forwarder_headers"
section = "Server Mechanics"
cli = ["--forwarder-headers"]
validator = validate_string_to_list
default = "SCRIPT_NAME"
desc = """\
A list containing headers and values that the front-end proxy
sets, to be used in WSGI environment.
If other headers listed in this list are not present in the request, they will be ignored.
This option can be used to transfer SCRIPT_NAME and REMOTE_USER.
The list should map upper-case header names to exact string
values. The value comparisons are case-sensitive, unlike the header
names, so make sure they're exactly what your front-end proxy sends.
It is important that your front-end proxy configuration ensures that
the headers defined here can not be passed directly from the client.
"""


class HeaderMap(Setting):
name = "header_map"
Expand All @@ -2356,11 +2391,12 @@ class HeaderMap(Setting):
The safe default ``drop`` is to silently drop headers that cannot be unambiguously mapped.
The value ``refuse`` will return an error if a request contains *any* such header.
The value ``dangerous`` matches the previous, not advisabble, behaviour of mapping different
The value ``dangerous`` matches the previous, not advisable, behaviour of mapping different
header field names into the same environ name.
The (at this time, not configurable) header `SCRIPT_NAME` is permitted
without consulting this setting, if it is received from an allowed forwarder.
If the source IP is permitted by ``forwarded-allow-ips``, *and* the header name is
present in ``forwarder-headers``, the header is mapped into environment regardless of
the state of this setting.
Use with care and only if necessary and after considering if your problem could
instead be solved by specifically renaming or rewriting only the intended headers
Expand Down
6 changes: 3 additions & 3 deletions gunicorn/http/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def parse_headers(self, data, from_trailer=False):
# handle scheme headers
scheme_header = False
secure_scheme_headers = {}
allowed_forwarder_headers = []
forwarder_headers = []
if from_trailer:
# nonsense. either a request is https from the beginning
# .. or we are just behind a proxy who does not remove conflicting trailers
Expand All @@ -86,7 +86,7 @@ def parse_headers(self, data, from_trailer=False):
not isinstance(self.peer_addr, tuple)
or self.peer_addr[0] in cfg.forwarded_allow_ips):
secure_scheme_headers = cfg.secure_scheme_headers
allowed_forwarder_headers = ["SCRIPT_NAME"]
forwarder_headers = cfg.forwarder_headers

# Parse headers into key/value pairs paying attention
# to continuation lines.
Expand Down Expand Up @@ -142,7 +142,7 @@ def parse_headers(self, data, from_trailer=False):
# HTTP_X_FORWARDED_FOR = 2001:db8::ha:cc:ed,127.0.0.1,::1
# Only modify after fixing *ALL* header transformations; network to wsgi env
if "_" in name:
if name in allowed_forwarder_headers:
if name in forwarder_headers:
# This forwarder may override our environment
pass
elif self.cfg.header_map == "dangerous":
Expand Down
24 changes: 20 additions & 4 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,32 @@ def test_str_validation():
pytest.raises(TypeError, c.set, "proc_name", 2)


def test_str_to_list_validation():
def test_str_to_addr_list_validation():
c = config.Config()
assert c.forwarded_allow_ips == ["127.0.0.1"]
c.set("forwarded_allow_ips", "127.0.0.1,192.168.0.1")
assert c.forwarded_allow_ips == ["127.0.0.1", "192.168.0.1"]
assert c.forwarded_allow_ips == ["127.0.0.1", "::1"]
c.set("forwarded_allow_ips", "127.0.0.1,192.0.2.1")
assert c.forwarded_allow_ips == ["127.0.0.1", "192.0.2.1"]
c.set("forwarded_allow_ips", "")
assert c.forwarded_allow_ips == []
c.set("forwarded_allow_ips", None)
assert c.forwarded_allow_ips == []
# demand addresses are specified unambiguously
pytest.raises(TypeError, c.set, "forwarded_allow_ips", 1)
# demand networks are specified unambiguously
pytest.raises(ValueError, c.set, "forwarded_allow_ips", "127.0.0")
# detect typos
pytest.raises(ValueError, c.set, "forwarded_allow_ips", "::f:")


def test_str_to_list():
c = config.Config()
assert c.forwarder_headers == ["SCRIPT_NAME"]
c.set("forwarder_headers", "SCRIPT_NAME,REMOTE_USER")
assert c.forwarder_headers == ["SCRIPT_NAME", "REMOTE_USER"]
c.set("forwarder_headers", "")
assert c.forwarder_headers == []
c.set("forwarder_headers", None)
assert c.forwarder_headers == []


def test_callable_validation():
Expand Down

0 comments on commit debe5bb

Please sign in to comment.