mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
fix(proxy): treat a malformed trusted_proxy_ranges entry as an undeclared topology
A list with an entry that is not an address or CIDR range no longer switches the per-source Admin UI sign-in limit on against the direct peer address, so a typo cannot make a shared ingress address the bucket for every user behind it Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
cfdf4fa5dc
commit
cacd12b87d
4 changed files with 28 additions and 14 deletions
|
|
@ -2880,7 +2880,7 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
|
|||
)
|
||||
trusted_proxy_ranges: list[str] | None = Field(
|
||||
None,
|
||||
description="CIDR ranges of trusted reverse proxies allowed to provide identity headers for header-based auth paths such as enable_oauth2_proxy_auth and custom_ui_sso_sign_in_handler, and whose X-Forwarded-For is used to attribute Admin UI sign-in attempts to a source address. Set it to an empty list when clients connect directly, so the peer address is the source. Left unset, the per-source sign-in limit is off.",
|
||||
description="CIDR ranges of trusted reverse proxies allowed to provide identity headers for header-based auth paths such as enable_oauth2_proxy_auth and custom_ui_sso_sign_in_handler, and whose X-Forwarded-For is used to attribute Admin UI sign-in attempts to a source address. Set it to an empty list when clients connect directly, so the peer address is the source. Left unset, or containing an entry that is not an address or CIDR range, the per-source sign-in limit is off.",
|
||||
)
|
||||
store_model_in_db: bool | None = Field(
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -120,9 +120,9 @@ def warn_login_counters_are_per_worker(num_workers: str) -> None:
|
|||
@cache
|
||||
def warn_source_login_limit_is_off() -> None:
|
||||
verbose_proxy_logger.warning(
|
||||
"%s is not set, so failed Admin UI sign-in attempts are limited per source address and username "
|
||||
"only. Set it to the address ranges of the proxies in front of LiteLLM, or to an empty list when "
|
||||
"clients connect directly, to also limit each source address across usernames.",
|
||||
"%s is not set or not a valid list of ranges, so failed Admin UI sign-in attempts are limited per "
|
||||
"source address and username only. Set it to the address ranges of the proxies in front of LiteLLM, "
|
||||
"or to an empty list when clients connect directly, to also limit each source address across usernames.",
|
||||
TRUSTED_PROXY_RANGES_KEY,
|
||||
)
|
||||
|
||||
|
|
@ -131,13 +131,16 @@ def declared_proxy_ranges(settings: Mapping[str, object]) -> tuple[str, ...] | N
|
|||
"""What the operator says fronts LiteLLM: the proxy ranges, an empty tuple for none, None when unsaid.
|
||||
|
||||
Only a declared topology makes the source address trustworthy enough to limit across usernames.
|
||||
An unset key, or a value that is not a list of ranges, leaves it unknown and the source scope off.
|
||||
An unset key, a value that is not a list of ranges, or a list with an entry that is not an address
|
||||
or range leaves it unknown and the source scope off.
|
||||
"""
|
||||
raw_ranges: Final = settings.get(TRUSTED_PROXY_RANGES_KEY)
|
||||
if isinstance(raw_ranges, (list, tuple, set)) and not raw_ranges:
|
||||
return ()
|
||||
cidrs: Final = tuple(normalize_cidr_ranges(raw_ranges, setting_name=TRUSTED_PROXY_RANGES_KEY))
|
||||
return cidrs or None
|
||||
if not cidrs or any(_parse_network(cidr, TRUSTED_PROXY_RANGES_KEY) is None for cidr in cidrs):
|
||||
return None
|
||||
return cidrs
|
||||
|
||||
|
||||
def _positive_int(raw: object, key: str, default: int) -> int:
|
||||
|
|
@ -176,13 +179,11 @@ def _parse_address(client_ip: str) -> ipaddress.IPv4Address | ipaddress.IPv6Addr
|
|||
return address
|
||||
|
||||
|
||||
def _parse_network(raw_range: str) -> _Network | None:
|
||||
def _parse_network(raw_range: str, setting_name: str = SOURCE_LIMIT_OVERRIDES_KEY) -> _Network | None:
|
||||
try:
|
||||
return ipaddress.ip_network(raw_range.strip(), strict=False)
|
||||
except ValueError:
|
||||
verbose_proxy_logger.warning(
|
||||
"Invalid address or range %r in %s; skipping", raw_range, SOURCE_LIMIT_OVERRIDES_KEY
|
||||
)
|
||||
verbose_proxy_logger.warning("Invalid address or range %r in %s; skipping", raw_range, setting_name)
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -934,10 +934,23 @@ async def test_an_empty_trusted_proxy_ranges_means_the_peer_is_the_client_and_th
|
|||
assert await _fail(throttle, username="user-99@corp.com") == "429", "the spray is stopped by the source limit"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("configured", [None, 5, {"10.0.0.0/8": True}, ["", " "]])
|
||||
@pytest.mark.parametrize(
|
||||
"configured",
|
||||
[
|
||||
None,
|
||||
5,
|
||||
{"10.0.0.0/8": True},
|
||||
["", " "],
|
||||
["not-a-range"],
|
||||
["10.0.0.0/8, 172.16.0.0/12"],
|
||||
["10.0.0.0/8", "10.0.0.0/33"],
|
||||
"10.0.0.0/8;172.16.0.0/12",
|
||||
],
|
||||
)
|
||||
def test_a_trusted_proxy_ranges_value_that_names_no_ranges_leaves_the_topology_unknown(configured):
|
||||
"""Only a real list of ranges or an explicit empty list counts as a declaration; anything else is the same
|
||||
as unset, so a typo cannot switch the source-wide block on behind a shared ingress."""
|
||||
"""Only a list of valid ranges or an explicit empty list counts as a declaration; anything else, including a
|
||||
list with one bad entry, is the same as unset, so a typo cannot switch the source-wide block on against
|
||||
the shared ingress address and lock out everyone behind it."""
|
||||
from litellm.proxy.auth.login_throttle import LoginThrottle, declared_proxy_ranges
|
||||
|
||||
settings = {"trusted_proxy_ranges": configured} if configured is not None else {}
|
||||
|
|
|
|||
2
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
2
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -26751,7 +26751,7 @@ export interface components {
|
|||
supported_db_objects?: components["schemas"]["SupportedDBObjectType"][] | null;
|
||||
/**
|
||||
* Trusted Proxy Ranges
|
||||
* @description CIDR ranges of trusted reverse proxies allowed to provide identity headers for header-based auth paths such as enable_oauth2_proxy_auth and custom_ui_sso_sign_in_handler, and whose X-Forwarded-For is used to attribute Admin UI sign-in attempts to a source address. Set it to an empty list when clients connect directly, so the peer address is the source. Left unset, the per-source sign-in limit is off.
|
||||
* @description CIDR ranges of trusted reverse proxies allowed to provide identity headers for header-based auth paths such as enable_oauth2_proxy_auth and custom_ui_sso_sign_in_handler, and whose X-Forwarded-For is used to attribute Admin UI sign-in attempts to a source address. Set it to an empty list when clients connect directly, so the peer address is the source. Left unset, or containing an entry that is not an address or CIDR range, the per-source sign-in limit is off.
|
||||
*/
|
||||
trusted_proxy_ranges?: string[] | null;
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue