fix(logging): scrub non-primitive tuple args (e.g. dicts) passed via %s (CWE-312)

Previously, only string elements in a log record's tuple args were scrubbed.
A dict like {"api_key": "sk-..."} passed as `logger.debug("cfg: %s", config)`
would reach any custom handler attached to LiteLLM loggers untouched.

Now any non-primitive tuple element (i.e. not bool/int/float/None) is
converted to its string repr and scrubbed before handlers see the record,
closing the parameterised-log bypass identified by Veria AI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Avani-prajapati 2026-05-20 18:03:22 +05:30
parent 0e092468c3
commit b7151d9b95
No known key found for this signature in database
2 changed files with 13 additions and 2 deletions

View file

@ -478,7 +478,11 @@ class CredentialScrubberFilter(logging.Filter):
}
elif isinstance(record.args, tuple):
record.args = tuple(
_scrub_secrets(str(a)) if isinstance(a, str) else a
(
_scrub_secrets(str(a))
if not isinstance(a, (bool, int, float, type(None)))
else a
)
for a in record.args
)
if record.exc_info and record.exc_info[1] is not None:

View file

@ -113,7 +113,13 @@ class TestCredentialScrubberFilter:
f = CredentialScrubberFilter()
record = self._make_record(
"vals=%s %s %s", (42, None, "api_key=sk-secret123456789")
"vals=%s %s %s %s",
(
42,
None,
"api_key=sk-secret123456789",
{"api_key": "sk-dictval12345678901"},
),
)
f.filter(record)
args = record.args
@ -121,6 +127,7 @@ class TestCredentialScrubberFilter:
assert args[0] == 42
assert args[1] is None
assert "sk-secret123456789" not in str(args[2])
assert "sk-dictval12345678901" not in str(args[3])
def test_no_args_no_crash(self):
# Branch: record.args is falsy (empty tuple)