Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support IP networks in allow lists #2390

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/source/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1066,8 +1066,8 @@ forwarded_allow_ips
* ``--forwarded-allow-ips STRING``
* ``127.0.0.1``

Front-end's IPs from which allowed to handle set secure headers.
(comma separate).
Front-end's IP addresses or networks from which allowed to handle
set secure headers. (comma separate).

Set to ``*`` to disable checking of Front-end IPs (useful for setups
where you don't know in advance the IP address of Front-end, but
Expand Down Expand Up @@ -1136,7 +1136,8 @@ proxy_allow_ips
* ``--proxy-allow-from``
* ``127.0.0.1``

Front-end's IPs from which allowed accept proxy requests (comma separate).
Front-end's IP addresses or networks from which allowed accept
proxy requests (comma separate).

Set to ``*`` to disable checking of Front-end IPs (useful for setups
where you don't know in advance the IP address of Front-end, but
Expand Down
17 changes: 12 additions & 5 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 @@ -525,6 +526,11 @@ def validate_hostport(val):
raise TypeError("Value must consist of: hostname:port")


def validate_string_to_network_list(val):
val = validate_string_to_list(val)
return [v if v == '*' else ipaddress.ip_network(v) for v in val]


def validate_reload_engine(val):
if val not in reloader_engines:
raise ConfigError("Invalid reload_engine: %r" % val)
Expand Down Expand Up @@ -1242,11 +1248,11 @@ class ForwardedAllowIPS(Setting):
section = "Server Mechanics"
cli = ["--forwarded-allow-ips"]
meta = "STRING"
validator = validate_string_to_list
validator = validate_string_to_network_list
default = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1")
desc = """\
Front-end's IPs from which allowed to handle set secure headers.
(comma separate).
Front-end's IP addresses or networks from which allowed to handle
set secure headers. (comma separate).

Set to ``*`` to disable checking of Front-end IPs (useful for setups
where you don't know in advance the IP address of Front-end, but
Expand Down Expand Up @@ -1915,10 +1921,11 @@ class ProxyAllowFrom(Setting):
name = "proxy_allow_ips"
section = "Server Mechanics"
cli = ["--proxy-allow-from"]
validator = validate_string_to_list
validator = validate_string_to_network_list
default = "127.0.0.1"
desc = """\
Front-end's IPs from which allowed accept proxy requests (comma separate).
Front-end's IP addresses or networks from which allowed accept
proxy requests (comma separate).

Set to ``*`` to disable checking of Front-end IPs (useful for setups
where you don't know in advance the IP address of Front-end, but
Expand Down
20 changes: 13 additions & 7 deletions gunicorn/http/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# See the NOTICE for more information.

import io
import ipaddress
import re
import socket
from errno import ENOTCONN
Expand Down Expand Up @@ -74,9 +75,11 @@ def parse_headers(self, data):
elif isinstance(self.unreader, SocketUnreader):
remote_addr = self.unreader.sock.getpeername()
if self.unreader.sock.family in (socket.AF_INET, socket.AF_INET6):
remote_host = remote_addr[0]
if remote_host in cfg.forwarded_allow_ips:
secure_scheme_headers = cfg.secure_scheme_headers
remote_host = ipaddress.ip_address(remote_addr[0])
for network in cfg.forwarded_allow_ips:
if network == '*' or remote_host in network:
secure_scheme_headers = cfg.secure_scheme_headers
break
elif self.unreader.sock.family == socket.AF_UNIX:
secure_scheme_headers = cfg.secure_scheme_headers

Expand Down Expand Up @@ -283,14 +286,17 @@ def proxy_protocol_access_check(self):
# check in allow list
if isinstance(self.unreader, SocketUnreader):
try:
remote_host = self.unreader.sock.getpeername()[0]
remote_addr = self.unreader.sock.getpeername()
except socket.error as e:
if e.args[0] == ENOTCONN:
raise ForbiddenProxyRequest("UNKNOW")
raise
if ("*" not in self.cfg.proxy_allow_ips and
remote_host not in self.cfg.proxy_allow_ips):
raise ForbiddenProxyRequest(remote_host)
remote_host = ipaddress.ip_address(remote_addr[0])
for network in self.cfg.proxy_allow_ips:
if network == '*' or remote_host in network:
break
else:
raise ForbiddenProxyRequest(remote_addr[0])

def parse_proxy_protocol(self, line):
bits = line.split()
Expand Down
16 changes: 13 additions & 3 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.

from ipaddress import IPv4Network, IPv6Network
import os
import re
import sys
Expand Down Expand Up @@ -147,11 +148,20 @@ def test_str_validation():
pytest.raises(TypeError, c.set, "proc_name", 2)


def test_str_to_list_validation():
def test_str_to_network_list_validation():
c = config.Config()
assert c.forwarded_allow_ips == ["127.0.0.1"]
assert c.forwarded_allow_ips == [IPv4Network("127.0.0.1/32")]
c.set("forwarded_allow_ips", "127.0.0.1,::1")
assert c.forwarded_allow_ips == [IPv4Network("127.0.0.1/32"),
IPv6Network("::1/128")]
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 == [IPv4Network("127.0.0.1/32"),
IPv4Network("192.168.0.1/32")]
c.set("forwarded_allow_ips", "127.0.0.0/8,192.168.0.1")
assert c.forwarded_allow_ips == [IPv4Network("127.0.0.0/8"),
IPv4Network("192.168.0.1/32")]
c.set("forwarded_allow_ips", "*")
assert c.forwarded_allow_ips == ["*"]
c.set("forwarded_allow_ips", "")
assert c.forwarded_allow_ips == []
c.set("forwarded_allow_ips", None)
Expand Down