diff --git a/intercept/apidump/apidump.go b/intercept/apidump/apidump.go index e8e6d893..71c79317 100644 --- a/intercept/apidump/apidump.go +++ b/intercept/apidump/apidump.go @@ -14,6 +14,7 @@ import ( "cdr.dev/slog/v3" + "github.com/coder/aibridge/utils" "github.com/coder/quartz" "github.com/google/uuid" "github.com/tidwall/pretty" @@ -186,7 +187,7 @@ func (d *dumper) writeRedactedHeaders(w io.Writer, headers http.Header, sensitiv } if isSensitive { - value = redactHeaderValue(value) + value = utils.MaskSecret(value) } _, err := fmt.Fprintf(w, "%s: %s\r\n", key, value) if err != nil { diff --git a/intercept/apidump/apidump_test.go b/intercept/apidump/apidump_test.go index 1fa3d7f9..e2600850 100644 --- a/intercept/apidump/apidump_test.go +++ b/intercept/apidump/apidump_test.go @@ -73,7 +73,7 @@ func TestBridgedMiddleware_RedactsSensitiveRequestHeaders(t *testing.T) { // Verify sensitive headers ARE present but redacted require.Contains(t, content, "Authorization: Bear...2345") require.Contains(t, content, "X-Api-Key: secr...alue") - require.Contains(t, content, "Cookie: sess...c123") // "session=abc123" is 14 chars, so first 4 + last 4 + require.Contains(t, content, "Cookie: se...23") // "session=abc123" is 14 chars, so first 2 + last 2 // Verify the full secret values are NOT present require.NotContains(t, content, "sk-secret-key-12345") @@ -133,8 +133,8 @@ func TestBridgedMiddleware_RedactsSensitiveResponseHeaders(t *testing.T) { // Verify sensitive headers are present but redacted require.Contains(t, content, "Set-Cookie: sess...cure") // Note: Go canonicalizes WWW-Authenticate to Www-Authenticate - // "Bearer realm=\"api\"" = 18 chars, first 4 = "Bear", last 4 = "api\"" - require.Contains(t, content, "Www-Authenticate: Bear...api\"") + // "Bearer realm=\"api\"" = 18 chars, first 2 = "Be", last 2 = "i\"" + require.Contains(t, content, "Www-Authenticate: Be...i\"") // Verify full secret values are NOT present require.NotContains(t, content, "secret123") diff --git a/intercept/apidump/headers.go b/intercept/apidump/headers.go index 0b4047dd..b6a69fa8 100644 --- a/intercept/apidump/headers.go +++ b/intercept/apidump/headers.go @@ -18,17 +18,3 @@ var sensitiveResponseHeaders = map[string]struct{}{ "Www-Authenticate": {}, "Proxy-Authenticate": {}, } - -// redactHeaderValue redacts a sensitive header value, showing only partial content. -// For values >= 8 bytes: shows first 4 and last 4 bytes with "..." in between. -// For values < 8 bytes: shows first and last byte with "..." in between. -func redactHeaderValue(value string) string { - if len(value) >= 8 { - return value[:4] + "..." + value[len(value)-4:] - } - if len(value) >= 2 { - return value[:1] + "..." + value[len(value)-1:] - } - // Single character or empty - just return as-is - return value -} diff --git a/intercept/apidump/headers_test.go b/intercept/apidump/headers_test.go index 181eae21..d78aa548 100644 --- a/intercept/apidump/headers_test.go +++ b/intercept/apidump/headers_test.go @@ -12,60 +12,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestRedactHeaderValue(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - input string - expected string - }{ - { - name: "empty string", - input: "", - expected: "", - }, - { - name: "single char", - input: "a", - expected: "a", - }, - { - name: "two chars", - input: "ab", - expected: "a...b", - }, - { - name: "seven chars", - input: "abcdefg", - expected: "a...g", - }, - { - name: "eight chars - threshold", - input: "abcdefgh", - expected: "abcd...efgh", - }, - { - name: "long value", - input: "Bearer sk-secret-key-12345", - expected: "Bear...2345", - }, - { - name: "realistic api key", - input: "sk-proj-abc123xyz789", - expected: "sk-p...z789", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - result := redactHeaderValue(tc.input) - require.Equal(t, tc.expected, result) - }) - } -} - func TestSensitiveHeaderLists(t *testing.T) { t.Parallel() @@ -140,7 +86,7 @@ func TestWriteRedactedHeaders(t *testing.T) { name: "sensitive header redacted", headers: http.Header{"Set-Cookie": {"session=abcdefghij"}}, sensitive: sensitiveResponseHeaders, - expected: "Set-Cookie: sess...ghij\r\n", + expected: "Set-Cookie: se...ij\r\n", }, { name: "multi-value header", diff --git a/provider/anthropic_test.go b/provider/anthropic_test.go index bc240a46..2db83f09 100644 --- a/provider/anthropic_test.go +++ b/provider/anthropic_test.go @@ -190,7 +190,7 @@ func TestAnthropic_CreateInterceptor_BYOK(t *testing.T) { setHeaders: map[string]string{}, wantXApiKey: "test-key", wantCredentialKind: intercept.CredentialKindCentralized, - wantCredentialHint: "***", + wantCredentialHint: "t...y", }, { name: "Messages_BYOK_BearerToken_And_APIKey", diff --git a/utils/mask.go b/utils/mask.go index 4c249c93..dc36af22 100644 --- a/utils/mask.go +++ b/utils/mask.go @@ -11,9 +11,8 @@ func MaskSecret(s string) string { runes := []rune(s) reveal := revealLength(len(runes)) - // If there's nothing safe to reveal, mask it all. - if reveal == 0 || reveal*2 >= len(runes) { - return "***" + if len(runes) <= reveal*2 { + return "..." } prefix := string(runes[:reveal]) @@ -28,6 +27,8 @@ func revealLength(n int) int { return 4 case n >= 10: return 2 + case n >= 5: + return 1 default: return 0 } diff --git a/utils/mask_test.go b/utils/mask_test.go index f71b8cf3..40263aae 100644 --- a/utils/mask_test.go +++ b/utils/mask_test.go @@ -16,8 +16,11 @@ func TestMaskSecret(t *testing.T) { expected string }{ {"empty", "", ""}, - {"short", "short", "***"}, - {"short_9_chars", "veryshort", "***"}, + {"single_char", "x", "..."}, + {"two_chars", "ab", "..."}, + {"four_chars", "abcd", "..."}, + {"short", "short", "s...t"}, + {"short_9_chars", "veryshort", "v...t"}, {"medium_15_chars", "thisisquitelong", "th...ng"}, {"long_api_key", "sk-ant-api03-abcdefgh", "sk-a...efgh"}, {"unicode", "hélloworld🌍!", "hé...🌍!"},