Skip to content

fix(llm): redact credentials from re-raised provider error messages#435

Open
VoidChecksum wants to merge 1 commit into
mainfrom
fix/llm-redact-error-secrets
Open

fix(llm): redact credentials from re-raised provider error messages#435
VoidChecksum wants to merge 1 commit into
mainfrom
fix/llm-redact-error-secrets

Conversation

@VoidChecksum
Copy link
Copy Markdown
Collaborator

Summary

Fixes a credential-disclosure-into-logs bug in the LLM factory's error
translation path.

_reraise_with_actionable_message and _reraise_if_connection_error in
packages/decepticon/decepticon/llm/factory.py interpolated the raw upstream
exception text (str(exc)) straight into the user-facing RuntimeError
(... \nUnderlying: {msg}). For LiteLLM/openai errors, str(exc) is built
from the provider's HTTP response body, which can reflect the
Authorization / x-api-key / Bearer value that was sent on the request.
Those RuntimeErrors propagate to the CLI and into logs — so a transient
upstream 4xx could leak a live API key to anyone reading the output.

Fix

Add _redact_secrets(text), a small regex scrubber applied to every place
these reraise paths interpolate the exception text. It redacts:

  • Bearer <token>Bearer [REDACTED]
  • sk-ant-* / sk-* provider key prefixes → [REDACTED]
  • authorization / api_key / x-api-key header values (JSON
    "authorization": "..." and kwarg api_key=... forms) → value replaced,
    key name kept for readability
  • generic long opaque tokens (≥24 chars) → [REDACTED]

Branch matching still runs on the raw message (HTTP status codes and
provider keywords aren't secret-shaped), so the actionable guidance — status
code, offending model id, and remediation hint — is preserved verbatim. Only
secret material is removed; non-secret text such as
model_group=anthropic/claude-opus-4-7 is left intact.

Tests

TestActionableErrorTranslation gains three cases:

  • Authorization: Bearer sk-ant-<fake> → secret redacted, credentials (401)
    guidance + model id still present
  • api_key=sk-<fake> → secret redacted, rejected the request (400) guidance
    still present
  • non-secret passthrough assertion for _redact_secrets

All placeholder tokens are obviously fake. Existing assertions (which check
guidance strings, not secret text) are unchanged.

Verification

  • uv run ruff check (both files) → clean
  • uv run ruff format --check (both files) → clean
  • uv run basedpyright packages/decepticon/decepticon/llm/factory.py → 0 errors
  • uv run pytest packages/decepticon/tests/unit/llm -q → 216 passed, 2 skipped

The actionable-error translation in `decepticon/llm/factory.py`
(`_reraise_with_actionable_message` and `_reraise_if_connection_error`)
interpolated the raw upstream exception text (`str(exc)`) into the
user-facing `RuntimeError` (e.g. `... Underlying: {msg}`).

For LiteLLM/openai errors `str(exc)` is constructed from the provider's
HTTP response body, which can echo back the `Authorization` / `x-api-key`
/ `Bearer` value that was sent on the request. Those RuntimeErrors
propagate to the CLI and into logs, so a transient upstream failure could
disclose a live API key to anyone reading the output.

Add `_redact_secrets(text)`, a small regex-based scrubber applied to every
place these reraise paths interpolate the exception text. It scrubs:

  - `Bearer <token>` -> `Bearer [REDACTED]`
  - `sk-ant-*` / `sk-*` provider key prefixes -> `[REDACTED]`
  - `authorization` / `api_key` / `x-api-key` header or kwarg values
    (JSON `"authorization": "..."` and `api_key=...` forms) -> value
    replaced with `[REDACTED]`, key name kept for readability
  - generic long opaque tokens (>=24 chars) -> `[REDACTED]`

Branch matching still runs against the raw message (HTTP status codes and
provider keywords are not secret-shaped), so the actionable guidance
(status code, offending model id, remediation hint) is preserved verbatim;
only secret material is removed. Non-secret text such as
`model_group=anthropic/claude-opus-4-7` is left intact.

Update `TestActionableErrorTranslation` to add coverage proving an
exception carrying `Authorization: Bearer sk-ant-...` (and one with
`api_key=sk-...`) is re-raised with the secret redacted while the
status-code/guidance remains, plus a non-secret passthrough assertion.
Tests use obviously-fake placeholder tokens only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant