Never insert a redaction marker that itself holds the API key
CI / ci (push) Successful in 1m29s
CI / ci (pull_request) Successful in 1m28s

llm.Redact is now the single redaction rule for ProviderError bodies and
the openai backstop. Each occurrence of the key becomes "[REDACTED]" only
when the result then holds no key; otherwise occurrences are deleted
until none remains, which also covers a marker that joins with its
neighbours to form the key. NewProviderError redacts again after
truncation so the "…" mark cannot complete the key.

The openai adapter drops its own redactKey and the withheld-text fallback,
which itself could hold a short key. redactedError.As now refuses a match
whose text holds the key, so errors.As cannot reach an unredacted error.

Tests cover the key "REDACTED" in a 500 body (ProviderError.Body and
Error after errors.As) and the key "A" in a malformed JSON body (every
unwrapped layer and every errors.As result).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 07:32:34 -04:00
co-authored by Claude Opus 5.5
parent cceeafd200
commit 68391d14c8
4 changed files with 158 additions and 33 deletions
+42
View File
@@ -117,3 +117,45 @@ func TestProviderErrorKeepsBoundedBodyWithoutSecret(t *testing.T) {
t.Fatalf("Body() has %d bytes, want %d", len(long.Body()), len(want))
}
}
func TestRedactNeverLeavesSecret(t *testing.T) {
cases := []struct{ s, secret, want string }{
{"key sk-1 and Bearer sk-1", "sk-1", "key [REDACTED] and Bearer [REDACTED]"},
{"no secret here", "sk-1", "no secret here"},
{"x", "", "x"},
// The marker holds the secret, so occurrences are deleted.
{"rejected REDACTED", "REDACTED", "rejected "},
{"A then A", "A", " then "},
// The marker joins with a neighbour to form the secret.
{"]]aa", "]a", ""},
// The marker holds the secret and deleting once leaves a new one.
{"DDAA", "DA", ""},
{"aabb", "ab", "a[REDACTED]b"},
}
for _, c := range cases {
if got := Redact(c.s, c.secret); got != c.want {
t.Errorf("Redact(%q, %q) = %q, want %q", c.s, c.secret, got, c.want)
}
}
}
func TestProviderErrorWithMarkerSecretNeverLeaksSecret(t *testing.T) {
const key = "REDACTED"
pe := NewProviderError(500, []byte(`{"error":"bad key REDACTED","echo":"Bearer REDACTED"}`), key)
for _, s := range []string{pe.Error(), pe.Body()} {
if strings.Contains(s, key) {
t.Fatalf("secret leaked: %q", s)
}
}
if want := `{"error":"bad key ","echo":"Bearer "}`; pe.Body() != want {
t.Fatalf("Body() = %q, want %q", pe.Body(), want)
}
}
func TestProviderErrorTruncationMarkCannotCompleteSecret(t *testing.T) {
const key = "z…"
body := strings.Repeat("a", MaxBodyExcerpt-1) + "z" + strings.Repeat("b", 10)
if pe := NewProviderError(500, []byte(body), key); strings.Contains(pe.Body(), key) {
t.Fatalf("secret leaked: %q", pe.Body())
}
}