mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-07 21:21:30 +08:00
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:
parent
b0d38928c8
commit
e52ac46e29
@ -407,12 +407,15 @@ def validate_list_of_existing_files(val):
|
|||||||
def validate_string_to_addr_list(val):
|
def validate_string_to_addr_list(val):
|
||||||
val = validate_string_to_list(val)
|
val = validate_string_to_list(val)
|
||||||
|
|
||||||
|
result = []
|
||||||
for addr in val:
|
for addr in val:
|
||||||
if addr == "*":
|
if addr == "*":
|
||||||
|
result.append(addr)
|
||||||
continue
|
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):
|
def validate_string_to_list(val):
|
||||||
@ -1278,8 +1281,11 @@ class ForwardedAllowIPS(Setting):
|
|||||||
validator = validate_string_to_addr_list
|
validator = validate_string_to_addr_list
|
||||||
default = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1,::1")
|
default = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1,::1")
|
||||||
desc = """\
|
desc = """\
|
||||||
Front-end's IPs from which allowed to handle set secure headers.
|
Front-end's IP addresses or networks from which allowed to handle
|
||||||
(comma separated).
|
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
|
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
|
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
|
validator = validate_string_to_addr_list
|
||||||
default = "127.0.0.1,::1"
|
default = "127.0.0.1,::1"
|
||||||
desc = """\
|
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
|
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
|
where you don't know in advance the IP address of front-end, but
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
# See the NOTICE for more information.
|
# See the NOTICE for more information.
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import ipaddress
|
||||||
import re
|
import re
|
||||||
import socket
|
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]")
|
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:
|
class Message:
|
||||||
def __init__(self, cfg, unreader, peer_addr):
|
def __init__(self, cfg, unreader, peer_addr):
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
@ -82,9 +99,8 @@ class Message:
|
|||||||
# nonsense. either a request is https from the beginning
|
# nonsense. either a request is https from the beginning
|
||||||
# .. or we are just behind a proxy who does not remove conflicting trailers
|
# .. or we are just behind a proxy who does not remove conflicting trailers
|
||||||
pass
|
pass
|
||||||
elif ('*' in cfg.forwarded_allow_ips or
|
elif (not isinstance(self.peer_addr, tuple)
|
||||||
not isinstance(self.peer_addr, tuple)
|
or _ip_in_allow_list(self.peer_addr[0], cfg.forwarded_allow_ips)):
|
||||||
or self.peer_addr[0] in cfg.forwarded_allow_ips):
|
|
||||||
secure_scheme_headers = cfg.secure_scheme_headers
|
secure_scheme_headers = cfg.secure_scheme_headers
|
||||||
forwarder_headers = cfg.forwarder_headers
|
forwarder_headers = cfg.forwarder_headers
|
||||||
|
|
||||||
@ -352,9 +368,8 @@ class Request(Message):
|
|||||||
|
|
||||||
def proxy_protocol_access_check(self):
|
def proxy_protocol_access_check(self):
|
||||||
# check in allow list
|
# check in allow list
|
||||||
if ("*" not in self.cfg.proxy_allow_ips and
|
if (isinstance(self.peer_addr, tuple) and
|
||||||
isinstance(self.peer_addr, tuple) and
|
not _ip_in_allow_list(self.peer_addr[0], self.cfg.proxy_allow_ips)):
|
||||||
self.peer_addr[0] not in self.cfg.proxy_allow_ips):
|
|
||||||
raise ForbiddenProxyRequest(self.peer_addr[0])
|
raise ForbiddenProxyRequest(self.peer_addr[0])
|
||||||
|
|
||||||
def parse_proxy_protocol(self, line):
|
def parse_proxy_protocol(self, line):
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
# This file is part of gunicorn released under the MIT license.
|
# This file is part of gunicorn released under the MIT license.
|
||||||
# See the NOTICE for more information.
|
# See the NOTICE for more information.
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@ -165,10 +166,30 @@ def test_str_validation():
|
|||||||
|
|
||||||
def test_str_to_addr_list_validation():
|
def test_str_to_addr_list_validation():
|
||||||
c = config.Config()
|
c = config.Config()
|
||||||
assert c.proxy_allow_ips == ["127.0.0.1", "::1"]
|
# Default values are now network objects
|
||||||
assert c.forwarded_allow_ips == ["127.0.0.1", "::1"]
|
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")
|
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", "")
|
c.set("forwarded_allow_ips", "")
|
||||||
assert c.forwarded_allow_ips == []
|
assert c.forwarded_allow_ips == []
|
||||||
c.set("forwarded_allow_ips", None)
|
c.set("forwarded_allow_ips", None)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user