From 0005428a760daeb7e5c0531afd8d8769157b3341 Mon Sep 17 00:00:00 2001 From: yucheng Date: Sat, 26 Sep 2026 13:30:56 +0000 Subject: [PATCH] fix(passthrough): mask dangling private keys after escaped newlines and at the preview cut Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../pass_through_endpoints.py | 4 ++- .../test_pass_through_endpoints.py | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index aae04fa96e2..69ff5388b4a 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -947,7 +947,9 @@ def _headers_without_body_framing(headers: httpx.Headers) -> httpx.Headers: ) -_DANGLING_PRIVATE_KEY: Final = re.compile(r"-----BEGIN[A-Z \-]*PRIVATE KEY----- ?[A-Za-z0-9+/= ]{32,}") +_DANGLING_PRIVATE_KEY: Final = re.compile( + r"-----BEGIN[A-Z \-]*PRIVATE KEY-----(?:[A-Za-z0-9+/= \\nr]{32,}|[A-Za-z0-9+/= \\nr]*$)" +) def _mask_dangling_private_key(text: str) -> str: diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py index 7ab281e7192..3154cc3bda9 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py @@ -8482,6 +8482,38 @@ async def test_upstream_error_body_for_log_sanitizes_before_slicing(): assert useful == sanitized[:PASSTHROUGH_UPSTREAM_ERROR_BODY_MAX_LOG_CHARS], useful[:200] +@pytest.mark.asyncio +async def test_upstream_error_body_for_log_masks_a_pem_after_a_json_escaped_newline(): + """A PEM embedded in a JSON-escaped error string keeps literal \\n separators; + the dangling mask must still cut it.""" + preview: Final = ( + b'{"error":"bad credentials -----BEGIN PRIVATE KEY-----\\nMIIEvQIBADANBg' + + b"kqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7" * 4 + + b'\\n","status":500}' + ) + output: Final = _upstream_error_body_for_log(preview, "utf-8", redact_secrets) + assert "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcw" not in output, output + assert "-----BEGIN" not in output, output + assert "bad credentials" in output, output + assert output.rstrip().endswith("REDACTED"), output + + +@pytest.mark.asyncio +async def test_upstream_error_body_for_log_masks_a_pem_header_at_the_preview_cut(): + """A header so close to the head cut that fewer than 32 key chars fit must + still be masked instead of leaking the partial key.""" + prefix: Final = "x" * (PASSTHROUGH_UPSTREAM_ERROR_BODY_MAX_LOG_CHARS - len("-----BEGIN PRIVATE KEY-----") - 11) + sanitized: Final = prefix + " -----BEGIN PRIVATE KEY----- MIIEvQIBAD" + "y" * 300 + key_start: Final = sanitized.index("MIIEvQIBAD") + assert key_start < PASSTHROUGH_UPSTREAM_ERROR_BODY_MAX_LOG_CHARS + assert PASSTHROUGH_UPSTREAM_ERROR_BODY_MAX_LOG_CHARS - key_start < 32 + output: Final = _upstream_error_body_for_log(sanitized.encode(), "utf-8", redact_secrets) + head: Final = output.removesuffix(_TRUNCATION_MARKER) + assert "-----BEGIN" not in head, head + assert "MIIEvQIBAD" not in head, head + assert head.rstrip().endswith("REDACTED"), head + + @pytest.mark.asyncio async def test_upstream_error_body_for_log_marks_bodies_past_the_scan_cap(): """A body beyond the 64 KiB raw scan cap must carry the truncation marker