mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
proxy: cap recursive secret redaction depth at 10
Match the cap on _redact_sensitive_litellm_params (the closest analog in the proxy, also recursive, key-name driven, returns a sentinel). The previous justification — bounded by operator-authored schema depth, JsonValue acyclic — is true today but is a property of the threat model, not an enforced invariant of the function. If a code path is ever added that pipes external input into general_settings (config import, migration tooling, JWT-driven settings, …) the assumption silently breaks. A local cap makes the invariant local. The cap branch fails closed: at _REDACT_SECRET_MAX_DEPTH the whole subtree is replaced with 'REDACTED' rather than returned verbatim. A future refactor that flips this to fail-open would let a deeply nested credential leak; the new regression test test_redact_secret_values_in_obj_fails_closed_at_max_depth guards against that. Updates the recursive_detector ignore-list rationale to point at the numeric cap rather than the structural argument.
This commit is contained in:
parent
ba36ad8e96
commit
e87b9e7827
3 changed files with 44 additions and 5 deletions
|
|
@ -15094,21 +15094,32 @@ def _is_secret_general_setting_field(field_name: str) -> bool:
|
|||
)
|
||||
|
||||
|
||||
def _redact_secret_values_in_obj(value: JsonValue) -> JsonValue:
|
||||
# Matches the cap on _redact_sensitive_litellm_params (the closest analog in the
|
||||
# proxy). Past this depth we fail closed by returning "REDACTED" for the whole
|
||||
# subtree rather than recursing further — better to over-redact a pathological
|
||||
# config than to silently return a deeply-nested credential verbatim
|
||||
_REDACT_SECRET_MAX_DEPTH = 10
|
||||
|
||||
|
||||
def _redact_secret_values_in_obj(value: JsonValue, depth: int = 0) -> JsonValue:
|
||||
"""Recursively redact secret leaves inside a structured field so a nested
|
||||
credential (e.g. aws_web_identity_token under database_args) is never
|
||||
returned to a non-admin, while non-secret siblings stay visible"""
|
||||
returned to a non-admin, while non-secret siblings stay visible. At
|
||||
_REDACT_SECRET_MAX_DEPTH the whole subtree is replaced with "REDACTED"
|
||||
so depth-overrun fails closed."""
|
||||
if depth >= _REDACT_SECRET_MAX_DEPTH:
|
||||
return "REDACTED"
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: (
|
||||
"REDACTED"
|
||||
if _is_secret_general_setting_field(key)
|
||||
else _redact_secret_values_in_obj(sub)
|
||||
else _redact_secret_values_in_obj(sub, depth + 1)
|
||||
)
|
||||
for key, sub in value.items()
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [_redact_secret_values_in_obj(item) for item in value]
|
||||
return [_redact_secret_values_in_obj(item, depth + 1) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ IGNORE_FUNCTIONS = [
|
|||
"_read_image_bytes", # max depth set.
|
||||
"_get_masked_values", # max depth set (default 20) to prevent infinite recursion while masking nested sensitive config dicts.
|
||||
"_redact_sensitive_litellm_params", # max depth set (default 10).
|
||||
"_redact_secret_values_in_obj", # config secret redaction; bounded by operator-authored general_settings schema depth, JsonValue is acyclic so no cycles possible.
|
||||
"_redact_secret_values_in_obj", # max depth set (default 10, _REDACT_SECRET_MAX_DEPTH); fails closed by returning "REDACTED" at the cap.
|
||||
"_resolve", # OCI: $ref resolver bounded by `resolving_stack` cycle guard.
|
||||
"resolve_oci_schema_anyof", # OCI: bounded by JSON-schema tree depth (no cycles possible in well-formed input).
|
||||
"sanitize_oci_schema", # OCI: bounded by JSON-schema tree depth.
|
||||
|
|
|
|||
|
|
@ -353,6 +353,34 @@ def test_redact_general_setting_value_recurses_list_of_dicts():
|
|||
)
|
||||
|
||||
|
||||
def test_redact_secret_values_in_obj_fails_closed_at_max_depth():
|
||||
"""Past _REDACT_SECRET_MAX_DEPTH the whole subtree is replaced with
|
||||
"REDACTED" rather than returned verbatim, so a secret buried below the cap
|
||||
can never leak via depth-overrun. A future refactor that flips the cap
|
||||
branch to fail-open would surface here."""
|
||||
from litellm.proxy import proxy_server as ps
|
||||
|
||||
# build a non-secret-keyed wrap chain deeper than the cap, with a real
|
||||
# secret at the bottom. Using a non-secret key ("wrap") forces the
|
||||
# recursor down the recursion branch instead of short-circuiting on the
|
||||
# key name itself.
|
||||
nested: object = {"aws_web_identity_token": "sk-leak-bottom"}
|
||||
for _ in range(ps._REDACT_SECRET_MAX_DEPTH + 2):
|
||||
nested = {"wrap": nested}
|
||||
|
||||
out = ps._redact_general_setting_value(
|
||||
"some_struct_field", nested, is_full_admin=False
|
||||
)
|
||||
# the secret must not survive anywhere in the returned tree
|
||||
assert "sk-leak-bottom" not in repr(out)
|
||||
|
||||
# full admin is unaffected by the cap — the value comes back untouched
|
||||
admin_out = ps._redact_general_setting_value(
|
||||
"some_struct_field", nested, is_full_admin=True
|
||||
)
|
||||
assert admin_out is nested
|
||||
|
||||
|
||||
def test_config_list_redacts_pass_through_secret_for_view_only(
|
||||
client, auth_as, mock_prisma, monkeypatch
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue