diff --git a/docs/source/settings.rst b/docs/source/settings.rst index 3b01f68f9..fa9c376dd 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -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 @@ -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 diff --git a/gunicorn/config.py b/gunicorn/config.py index da5803222..cac8a3d0c 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -9,6 +9,7 @@ import copy import grp import inspect +import ipaddress import os import pwd import re @@ -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) @@ -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 @@ -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 diff --git a/gunicorn/http/message.py b/gunicorn/http/message.py index 93ecf3b3c..0a2016ee6 100644 --- a/gunicorn/http/message.py +++ b/gunicorn/http/message.py @@ -4,6 +4,7 @@ # See the NOTICE for more information. import io +import ipaddress import re import socket from errno import ENOTCONN @@ -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 @@ -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() diff --git a/tests/test_config.py b/tests/test_config.py index 92cb73c37..2216f56f8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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 @@ -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)