feat: support CIDR networks in forwarded_allow_ips and proxy_allow_ips

Use Python's ipaddress module to support IP networks in allow lists.
Individual IP addresses are converted to /32 (IPv4) or /128 (IPv6)
networks. CIDR notation (e.g., 192.168.0.0/16) is now supported.

Fixes #1485
Closes #2390
This commit is contained in:
Benoit Chesneau 2026-01-23 11:09:26 +01:00
parent b0d38928c8
commit e52ac46e29
3 changed files with 60 additions and 14 deletions

View File

@ -407,12 +407,15 @@ def validate_list_of_existing_files(val):
def validate_string_to_addr_list(val):
val = validate_string_to_list(val)
result = []
for addr in val:
if addr == "*":
result.append(addr)
continue
_vaid_ip = ipaddress.ip_address(addr)
# Support both single IPs and CIDR networks
result.append(ipaddress.ip_network(addr, strict=False))
return val
return result
def validate_string_to_list(val):
@ -1278,8 +1281,11 @@ class ForwardedAllowIPS(Setting):
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.
(comma separated).
Front-end's IP addresses or networks from which allowed to handle
set secure headers. (comma separated).
Supports both individual IP addresses (e.g., ``192.168.1.1``) and
CIDR networks (e.g., ``192.168.0.0/16``).
Set to ``*`` to disable checking of front-end IPs. This is useful for setups
where you don't know in advance the IP address of front-end, but
@ -2094,7 +2100,11 @@ class ProxyAllowFrom(Setting):
validator = validate_string_to_addr_list
default = "127.0.0.1,::1"
desc = """\
Front-end's IPs from which allowed accept proxy requests (comma separated).
Front-end's IP addresses or networks from which allowed accept
proxy requests (comma separated).
Supports both individual IP addresses (e.g., ``192.168.1.1``) and
CIDR networks (e.g., ``192.168.0.0/16``).
Set to ``*`` to disable checking of front-end IPs. This is useful for setups
where you don't know in advance the IP address of front-end, but

View File

@ -3,6 +3,7 @@
# See the NOTICE for more information.
import io
import ipaddress
import re
import socket
@ -30,6 +31,22 @@ VERSION_RE = re.compile(r"HTTP/(\d)\.(\d)")
RFC9110_5_5_INVALID_AND_DANGEROUS = re.compile(r"[\0\r\n]")
def _ip_in_allow_list(ip_str, allow_list):
"""Check if IP address is in the allow list (which may contain networks)."""
if '*' in allow_list:
return True
try:
ip = ipaddress.ip_address(ip_str)
except ValueError:
return False
for network in allow_list:
if network == '*':
return True
if ip in network:
return True
return False
class Message:
def __init__(self, cfg, unreader, peer_addr):
self.cfg = cfg
@ -82,9 +99,8 @@ class Message:
# nonsense. either a request is https from the beginning
# .. or we are just behind a proxy who does not remove conflicting trailers
pass
elif ('*' in cfg.forwarded_allow_ips or
not isinstance(self.peer_addr, tuple)
or self.peer_addr[0] in cfg.forwarded_allow_ips):
elif (not isinstance(self.peer_addr, tuple)
or _ip_in_allow_list(self.peer_addr[0], cfg.forwarded_allow_ips)):
secure_scheme_headers = cfg.secure_scheme_headers
forwarder_headers = cfg.forwarder_headers
@ -352,9 +368,8 @@ class Request(Message):
def proxy_protocol_access_check(self):
# check in allow list
if ("*" not in self.cfg.proxy_allow_ips and
isinstance(self.peer_addr, tuple) and
self.peer_addr[0] not in self.cfg.proxy_allow_ips):
if (isinstance(self.peer_addr, tuple) and
not _ip_in_allow_list(self.peer_addr[0], self.cfg.proxy_allow_ips)):
raise ForbiddenProxyRequest(self.peer_addr[0])
def parse_proxy_protocol(self, line):

View File

@ -2,6 +2,7 @@
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
import ipaddress
import os
import re
import sys
@ -165,10 +166,30 @@ def test_str_validation():
def test_str_to_addr_list_validation():
c = config.Config()
assert c.proxy_allow_ips == ["127.0.0.1", "::1"]
assert c.forwarded_allow_ips == ["127.0.0.1", "::1"]
# Default values are now network objects
assert c.proxy_allow_ips == [
ipaddress.ip_network("127.0.0.1/32"),
ipaddress.ip_network("::1/128")
]
assert c.forwarded_allow_ips == [
ipaddress.ip_network("127.0.0.1/32"),
ipaddress.ip_network("::1/128")
]
# Single IPs are converted to /32 or /128 networks
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"]
assert c.forwarded_allow_ips == [
ipaddress.ip_network("127.0.0.1/32"),
ipaddress.ip_network("192.0.2.1/32")
]
# CIDR networks are supported
c.set("forwarded_allow_ips", "127.0.0.0/8,192.168.0.0/16")
assert c.forwarded_allow_ips == [
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("192.168.0.0/16")
]
# Wildcard is preserved as string
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)