mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
chore(proxy): close residual config-read disclosure gaps from review
- /config/field/info also blocks alert_to_webhook_url and pass_through_endpoints (Slack webhook URL + forwarded upstream auth headers), which a read-only admin could otherwise read despite the dedicated masked endpoints. - mask *_HEADERS callback vars (OTEL_HEADERS, GENERIC_LOGGER_HEADERS) for non-admins; these can carry bearer tokens but have no credential-looking name segment. - keep IPv6 brackets when redacting a connection-URL password so the value stays well-formed.
This commit is contained in:
parent
48c6138e71
commit
041e96ae5c
7 changed files with 56 additions and 8 deletions
|
|
@ -203,9 +203,10 @@ def mask_url_credentials(value: Any) -> Any:
|
|||
return value
|
||||
if parts.password is None:
|
||||
return value
|
||||
netloc = (
|
||||
f"{parts.username or ''}:{_default_masker.mask_char * 4}@{parts.hostname or ''}"
|
||||
)
|
||||
host = parts.hostname or ""
|
||||
if ":" in host: # IPv6 literal needs its brackets back after urlsplit strips them
|
||||
host = f"[{host}]"
|
||||
netloc = f"{parts.username or ''}:{_default_masker.mask_char * 4}@{host}"
|
||||
if parts.port is not None:
|
||||
netloc += f":{parts.port}"
|
||||
return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
|
||||
|
|
|
|||
|
|
@ -643,7 +643,14 @@ def process_callback(
|
|||
env_vars_dict[_var] = environment_variables.get(_var, None)
|
||||
|
||||
if mask_sensitive:
|
||||
sensitive_keys = {k for k in env_vars_dict if _is_sensitive_callback_var(k)}
|
||||
# *_HEADERS callback vars (e.g. OTEL_HEADERS, GENERIC_LOGGER_HEADERS) can
|
||||
# carry bearer tokens but have no credential-looking segment, so mask
|
||||
# them alongside the segment-matched sensitive vars.
|
||||
sensitive_keys = {
|
||||
k
|
||||
for k in env_vars_dict
|
||||
if _is_sensitive_callback_var(k) or k.upper().endswith("_HEADERS")
|
||||
}
|
||||
env_vars_dict = mask_sensitive_keys(env_vars_dict, sensitive_keys)
|
||||
env_vars_dict = {k: mask_url_credentials(v) for k, v in env_vars_dict.items()}
|
||||
|
||||
|
|
|
|||
|
|
@ -14254,8 +14254,15 @@ async def update_config_general_settings(
|
|||
# general_settings fields that hold credentials. These must never be returned
|
||||
# verbatim through the read endpoint: it is reachable by PROXY_ADMIN_VIEW_ONLY,
|
||||
# and the master_key in particular would let a read-only caller mint a full
|
||||
# admin key.
|
||||
_SECRET_CONFIG_GENERAL_SETTINGS_FIELDS = {"master_key", "database_url"}
|
||||
# admin key. alert_to_webhook_url carries Slack webhook URLs and
|
||||
# pass_through_endpoints carries forwarded upstream auth headers, both of which
|
||||
# are otherwise masked on their dedicated endpoints.
|
||||
_SECRET_CONFIG_GENERAL_SETTINGS_FIELDS = {
|
||||
"master_key",
|
||||
"database_url",
|
||||
"alert_to_webhook_url",
|
||||
"pass_through_endpoints",
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
|
|
|
|||
|
|
@ -2856,8 +2856,10 @@ async def test_get_config_callbacks_environment_variables(client_no_auth):
|
|||
assert otel_vars["OTEL_EXPORTER"] == "otlp"
|
||||
assert "OTEL_ENDPOINT" in otel_vars
|
||||
assert otel_vars["OTEL_ENDPOINT"] == "http://localhost:4317"
|
||||
# OTEL_HEADERS can carry bearer tokens, so it is masked for non-admins.
|
||||
assert "OTEL_HEADERS" in otel_vars
|
||||
assert otel_vars["OTEL_HEADERS"] == "key=value"
|
||||
assert otel_vars["OTEL_HEADERS"] != "key=value"
|
||||
assert "*" in otel_vars["OTEL_HEADERS"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -190,3 +190,10 @@ def test_mask_url_credentials_leaves_non_credentialed_values_untouched():
|
|||
assert mask_url_credentials("plain-secret-value") == "plain-secret-value"
|
||||
assert mask_url_credentials(None) is None
|
||||
assert mask_url_credentials(1234) == 1234
|
||||
|
||||
|
||||
def test_mask_url_credentials_preserves_ipv6_brackets():
|
||||
"""IPv6 hosts keep their brackets so the redacted URL stays well-formed."""
|
||||
out = mask_url_credentials("redis://:supersecretpw@[::1]:6379")
|
||||
assert out == "redis://:****@[::1]:6379"
|
||||
assert "supersecretpw" not in out
|
||||
|
|
|
|||
|
|
@ -142,6 +142,27 @@ def test_process_callback_masks_url_embedded_credentials(mock_get_env_vars):
|
|||
assert "topsecretpw" not in result["variables"]["GENERIC_LOGGER_URL"]
|
||||
|
||||
|
||||
@patch(
|
||||
"litellm.proxy.common_utils.callback_utils.CustomLogger.get_callback_env_vars",
|
||||
return_value=["OTEL_HEADERS", "OTEL_ENDPOINT"],
|
||||
)
|
||||
def test_process_callback_masks_headers_vars(mock_get_env_vars):
|
||||
"""*_HEADERS callback vars can carry bearer tokens, so they are masked even
|
||||
though the name has no credential-looking segment; the endpoint stays clear."""
|
||||
result = process_callback(
|
||||
_callback="otel",
|
||||
callback_type="success",
|
||||
environment_variables={
|
||||
"OTEL_HEADERS": "Authorization=Bearer sk-otel-secret-token",
|
||||
"OTEL_ENDPOINT": "http://collector.internal:4317",
|
||||
},
|
||||
mask_sensitive=True,
|
||||
)
|
||||
assert "sk-otel-secret-token" not in result["variables"]["OTEL_HEADERS"]
|
||||
assert "*" in result["variables"]["OTEL_HEADERS"]
|
||||
assert result["variables"]["OTEL_ENDPOINT"] == "http://collector.internal:4317"
|
||||
|
||||
|
||||
def test_normalize_callback_names_none_returns_empty_list():
|
||||
assert normalize_callback_names(None) == []
|
||||
assert normalize_callback_names([]) == []
|
||||
|
|
|
|||
|
|
@ -159,7 +159,10 @@ async def test_pass_through_get_masks_headers_for_non_admin_only():
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("field_name", ["master_key", "database_url"])
|
||||
@pytest.mark.parametrize(
|
||||
"field_name",
|
||||
["master_key", "database_url", "alert_to_webhook_url", "pass_through_endpoints"],
|
||||
)
|
||||
async def test_config_field_info_blocks_secret_fields(field_name):
|
||||
from litellm.proxy.proxy_server import get_config_general_settings
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue