mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
chore(proxy): also redact secrets in pass-through target query string
A pass-through target can carry an upstream key as a query parameter (e.g. https://host/v1?subscription-key=...), not only in URL userinfo. Add mask_url_query_values to redact all target query-param values (keeping names) for non-admin callers, alongside the existing userinfo redaction.
This commit is contained in:
parent
93ab57e478
commit
3b1b13fc94
4 changed files with 60 additions and 8 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import re
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
||||
|
||||
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER
|
||||
|
||||
|
|
@ -212,6 +212,34 @@ def mask_url_credentials(value: Any) -> Any:
|
|||
return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
|
||||
|
||||
|
||||
def mask_url_query_values(value: Any) -> Any:
|
||||
"""Redact the values of every query parameter in a URL while keeping the
|
||||
parameter names, so a secret passed as a query param (e.g.
|
||||
``https://host/v1?api_key=secret``) is not returned verbatim. The query
|
||||
keys can be arbitrary (``apiKey``, ``subscription-key``), so all values are
|
||||
masked rather than only the credential-looking ones.
|
||||
|
||||
Non-URL strings, and URLs without a query string, are returned unchanged.
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
try:
|
||||
parts = urlsplit(value)
|
||||
except ValueError:
|
||||
return value
|
||||
if not parts.query:
|
||||
return value
|
||||
masked_query = urlencode(
|
||||
[
|
||||
(key, _default_masker.mask_char * 4)
|
||||
for key, _ in parse_qsl(parts.query, keep_blank_values=True)
|
||||
]
|
||||
)
|
||||
return urlunsplit(
|
||||
(parts.scheme, parts.netloc, parts.path, masked_query, parts.fragment)
|
||||
)
|
||||
|
||||
|
||||
# Usage example:
|
||||
"""
|
||||
masker = SensitiveDataMasker()
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
|||
from litellm.litellm_core_utils.sensitive_data_masker import (
|
||||
mask_sensitive_keys,
|
||||
mask_url_credentials,
|
||||
mask_url_query_values,
|
||||
)
|
||||
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
|
||||
from litellm.passthrough import BasePassthroughUtils
|
||||
|
|
@ -2642,11 +2643,12 @@ def _mask_pass_through_endpoint_secrets(
|
|||
) -> PassThroughGenericEndpoint:
|
||||
"""Return a copy of the endpoint with forwarded credentials masked.
|
||||
|
||||
A pass-through endpoint carries upstream credentials in three places: the
|
||||
A pass-through endpoint carries upstream credentials in several places: the
|
||||
forwarded ``headers``, the ``default_query_params`` (an API key is often
|
||||
passed as a query param), and the ``target`` URL userinfo. Names/structure
|
||||
stay visible so a read-only caller can still see what is configured; the
|
||||
values are redacted.
|
||||
passed as a query param), and the ``target`` URL, which can embed a secret
|
||||
both in its userinfo and as a query parameter. Names/structure stay visible
|
||||
so a read-only caller can still see what is configured; the values are
|
||||
redacted.
|
||||
"""
|
||||
masked = endpoint.model_copy(deep=True)
|
||||
if masked.headers:
|
||||
|
|
@ -2656,7 +2658,7 @@ def _mask_pass_through_endpoint_secrets(
|
|||
masked.default_query_params, set(masked.default_query_params.keys())
|
||||
)
|
||||
if masked.target:
|
||||
masked.target = mask_url_credentials(masked.target)
|
||||
masked.target = mask_url_query_values(mask_url_credentials(masked.target))
|
||||
return masked
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -209,3 +209,19 @@ def test_fully_redacting_masker_masks_short_secret_values():
|
|||
assert set(masked["api_key"]) == {"*"}
|
||||
assert masked["password"] == "*"
|
||||
assert masked["port"] == 6379
|
||||
|
||||
|
||||
from litellm.litellm_core_utils.sensitive_data_masker import mask_url_query_values
|
||||
|
||||
|
||||
def test_mask_url_query_values_redacts_all_query_values_keeps_names():
|
||||
"""Query-param values (which may carry an upstream key under any name) are
|
||||
masked while names and the rest of the URL are preserved."""
|
||||
out = mask_url_query_values("https://host.example/v1?api_key=secret123&x=v1")
|
||||
assert "secret123" not in out
|
||||
assert "api_key=" in out and "x=" in out # names preserved
|
||||
assert out.startswith("https://host.example/v1?")
|
||||
# No query string / non-URL inputs are unchanged.
|
||||
assert mask_url_query_values("https://host.example/v1") == "https://host.example/v1"
|
||||
assert mask_url_query_values("not-a-url") == "not-a-url"
|
||||
assert mask_url_query_values(None) is None
|
||||
|
|
|
|||
|
|
@ -134,10 +134,13 @@ def test_mask_pass_through_endpoint_secrets_redacts_all_credential_fields():
|
|||
_mask_pass_through_endpoint_secrets,
|
||||
)
|
||||
|
||||
target = (
|
||||
"https://user:targetpw@upstream.example.com/v1?subscription-key=tgt-qp-secret"
|
||||
)
|
||||
ep = _make_endpoint(
|
||||
headers={"Authorization": "Bearer sk-upstream-secret-token"},
|
||||
default_query_params={"api_key": "qp-secret-key-value"},
|
||||
target="https://user:targetpw@upstream.example.com/v1",
|
||||
target=target,
|
||||
)
|
||||
masked = _mask_pass_through_endpoint_secrets(ep)
|
||||
|
||||
|
|
@ -147,11 +150,14 @@ def test_mask_pass_through_endpoint_secrets_redacts_all_credential_fields():
|
|||
# default_query_params value masked, name preserved.
|
||||
assert "api_key" in masked.default_query_params
|
||||
assert "qp-secret-key-value" not in masked.default_query_params["api_key"]
|
||||
# target URL userinfo password redacted.
|
||||
# target URL userinfo password AND query-param secret both redacted.
|
||||
assert "targetpw" not in masked.target
|
||||
assert "tgt-qp-secret" not in masked.target
|
||||
assert "subscription-key" in masked.target # param name preserved
|
||||
# Original object is not mutated.
|
||||
assert ep.headers["Authorization"] == "Bearer sk-upstream-secret-token"
|
||||
assert ep.default_query_params["api_key"] == "qp-secret-key-value"
|
||||
assert ep.target == target
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue