mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
fix(utils): redact credentials nested inside lists and tuples
redact_credentials_in_payload only recursed into mappings, so a credential-named key one level inside a list or tuple, the shape extra_body and metadata routinely carry, still reached stdout under set_verbose. Rebuild sequences element by element too, keeping the container's own type so the printed repr is unchanged apart from the secret.
This commit is contained in:
parent
64601fd7ae
commit
0a62195db2
3 changed files with 60 additions and 15 deletions
|
|
@ -1,4 +1,4 @@
|
|||
from collections.abc import Mapping
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, Final
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
|
@ -225,8 +225,8 @@ def redact_credentials_in_payload(data: Mapping[str, object]) -> Mapping[str, ob
|
|||
:func:`mask_credentials_in_payload`, no prefix or suffix of the secret survives
|
||||
and non-string secrets are covered too, which is what a payload rendered
|
||||
straight to stdout needs. ``None`` is preserved so an unset credential still
|
||||
reads as unset, and non-mapping containers are left alone so the caller's
|
||||
``repr`` is unchanged.
|
||||
reads as unset, and lists and tuples are rebuilt element by element so a
|
||||
credential nested inside one is caught as well.
|
||||
"""
|
||||
return _redact_mapping(data, 0)
|
||||
|
||||
|
|
@ -242,9 +242,18 @@ def _redact_entry(key: str, value: object, depth: int) -> object:
|
|||
return REDACTED
|
||||
if isinstance(value, Mapping):
|
||||
return _redact_mapping(value, depth + 1)
|
||||
if isinstance(value, (list, tuple)):
|
||||
return _redact_sequence(value, depth + 1)
|
||||
return value
|
||||
|
||||
|
||||
def _redact_sequence(values: Sequence[object], depth: int) -> Sequence[object]:
|
||||
if depth >= DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER:
|
||||
return values
|
||||
redacted: Final = tuple(_redact_entry("", item, depth) for item in values)
|
||||
return redacted if isinstance(values, tuple) else list(redacted)
|
||||
|
||||
|
||||
# Usage example:
|
||||
"""
|
||||
masker = SensitiveDataMasker()
|
||||
|
|
|
|||
|
|
@ -348,3 +348,27 @@ def test_redact_credentials_in_payload_leaves_no_fragment_of_the_secret():
|
|||
assert result["max_tokens"] == 17
|
||||
assert result["temperature"] == 0.25
|
||||
assert result["api_base"] is None
|
||||
|
||||
|
||||
def test_redact_credentials_in_payload_reaches_credentials_nested_in_sequences():
|
||||
"""Free-form kwargs like extra_body and metadata routinely carry lists of dicts, so a
|
||||
credential hiding one level inside a list or tuple must be replaced too, while the
|
||||
surrounding container keeps its type and every ordinary element stays verbatim."""
|
||||
from litellm.litellm_core_utils.sensitive_data_masker import redact_credentials_in_payload
|
||||
|
||||
result = redact_credentials_in_payload(
|
||||
{
|
||||
"extra_body": {"providers": [{"name": "openai", "api_key": "sk-fake-lit6823-in-a-list"}]},
|
||||
"metadata": {"upstreams": ({"aws_secret_access_key": "fake-aws-in-a-tuple"},)},
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
}
|
||||
)
|
||||
|
||||
assert "sk-fake-lit6823-in-a-list" not in str(result)
|
||||
assert "fake-aws-in-a-tuple" not in str(result)
|
||||
assert result["extra_body"]["providers"][0]["api_key"] == "REDACTED"
|
||||
assert result["extra_body"]["providers"][0]["name"] == "openai"
|
||||
assert isinstance(result["extra_body"]["providers"], list)
|
||||
assert result["metadata"]["upstreams"][0]["aws_secret_access_key"] == "REDACTED"
|
||||
assert isinstance(result["metadata"]["upstreams"], tuple)
|
||||
assert result["messages"] == [{"role": "user", "content": "hello"}]
|
||||
|
|
|
|||
|
|
@ -5808,14 +5808,15 @@ class TestIsVisionExplicitlyDisabled:
|
|||
|
||||
|
||||
class TestVerboseRequestLineRedaction:
|
||||
"""`litellm.set_verbose = True` echoes the caller's kwargs to stdout, so a credential
|
||||
kwarg lands in whatever collects stdout: a terminal, a container log drain, a CI job
|
||||
log. Credential-named kwargs must not survive that echo, while ordinary params still
|
||||
must, or the line stops telling the developer what they called."""
|
||||
"""`litellm.set_verbose = True` echoes the caller's kwargs back as a `litellm.completion(...)`
|
||||
line on stdout, so a credential kwarg lands in whatever collects stdout: a terminal, a
|
||||
container log drain, a CI job log. Credential-named kwargs must not survive that echo,
|
||||
at any nesting depth, while ordinary params still must, or the line stops telling the
|
||||
developer what they called."""
|
||||
|
||||
FAKE_API_KEY: Final = "sk-fake-lit6823-0000000000000000"
|
||||
|
||||
def _verbose_stdout(self, capsys, monkeypatch, **kwargs) -> str:
|
||||
def _verbose_request_line(self, capsys, monkeypatch, **kwargs) -> str:
|
||||
monkeypatch.setattr(litellm, "set_verbose", True)
|
||||
monkeypatch.setattr("litellm._logging.set_verbose", True)
|
||||
capsys.readouterr()
|
||||
|
|
@ -5826,17 +5827,17 @@ class TestVerboseRequestLineRedaction:
|
|||
**kwargs,
|
||||
)
|
||||
captured: Final = capsys.readouterr()
|
||||
return captured.out + captured.err
|
||||
return "\n".join(line for line in (captured.out + captured.err).splitlines() if "litellm.completion(" in line)
|
||||
|
||||
def test_api_key_never_reaches_stdout(self, capsys, monkeypatch):
|
||||
printed: Final = self._verbose_stdout(capsys, monkeypatch, api_key=self.FAKE_API_KEY)
|
||||
def test_api_key_never_reaches_the_request_line(self, capsys, monkeypatch):
|
||||
printed: Final = self._verbose_request_line(capsys, monkeypatch, api_key=self.FAKE_API_KEY)
|
||||
|
||||
assert "Request to litellm:" in printed
|
||||
assert "litellm.completion(" in printed
|
||||
assert self.FAKE_API_KEY not in printed
|
||||
assert "api_key='REDACTED'" in printed
|
||||
|
||||
def test_credential_headers_never_reach_stdout(self, capsys, monkeypatch):
|
||||
printed: Final = self._verbose_stdout(
|
||||
def test_credential_headers_never_reach_the_request_line(self, capsys, monkeypatch):
|
||||
printed: Final = self._verbose_request_line(
|
||||
capsys,
|
||||
monkeypatch,
|
||||
api_key=self.FAKE_API_KEY,
|
||||
|
|
@ -5847,8 +5848,19 @@ class TestVerboseRequestLineRedaction:
|
|||
assert "'Authorization': 'REDACTED'" in printed
|
||||
assert "'x-request-id': 'abc123'" in printed
|
||||
|
||||
def test_credentials_nested_in_a_list_never_reach_the_request_line(self, capsys, monkeypatch):
|
||||
printed: Final = self._verbose_request_line(
|
||||
capsys,
|
||||
monkeypatch,
|
||||
api_key=self.FAKE_API_KEY,
|
||||
extra_body={"providers": [{"name": "openai", "api_key": "sk-fake-lit6823-nested"}]},
|
||||
)
|
||||
|
||||
assert "sk-fake-lit6823-nested" not in printed
|
||||
assert "'name': 'openai'" in printed
|
||||
|
||||
def test_ordinary_params_still_printed(self, capsys, monkeypatch):
|
||||
printed: Final = self._verbose_stdout(
|
||||
printed: Final = self._verbose_request_line(
|
||||
capsys, monkeypatch, api_key=self.FAKE_API_KEY, max_tokens=17, temperature=0.25
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue