From e1a5d94d8d313111593c6fe231ed4156ac42a7d2 Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Thu, 9 Apr 2026 18:11:08 +0000 Subject: [PATCH 1/5] fix: change mask-secret function --- provider/anthropic_test.go | 2 +- utils/mask.go | 7 +++---- utils/mask_test.go | 6 ++++-- 3 files changed, 8 insertions(+), 7 deletions(-) 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..261473c5 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 s } prefix := string(runes[:reveal]) @@ -29,6 +28,6 @@ func revealLength(n int) int { case n >= 10: return 2 default: - return 0 + return 1 } } diff --git a/utils/mask_test.go b/utils/mask_test.go index f71b8cf3..b3450ee7 100644 --- a/utils/mask_test.go +++ b/utils/mask_test.go @@ -16,8 +16,10 @@ func TestMaskSecret(t *testing.T) { expected string }{ {"empty", "", ""}, - {"short", "short", "***"}, - {"short_9_chars", "veryshort", "***"}, + {"single_char", "x", "x"}, + {"two_chars", "ab", "ab"}, + {"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é...🌍!"}, From 6c88bec443636f9496bbdae713636e7ca1e3a00e Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Fri, 10 Apr 2026 13:28:43 +0000 Subject: [PATCH 2/5] fix: change mask-secret function --- utils/mask.go | 6 ++++-- utils/mask_test.go | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/utils/mask.go b/utils/mask.go index 261473c5..dc36af22 100644 --- a/utils/mask.go +++ b/utils/mask.go @@ -12,7 +12,7 @@ func MaskSecret(s string) string { reveal := revealLength(len(runes)) if len(runes) <= reveal*2 { - return s + return "..." } prefix := string(runes[:reveal]) @@ -27,7 +27,9 @@ func revealLength(n int) int { return 4 case n >= 10: return 2 - default: + case n >= 5: return 1 + default: + return 0 } } diff --git a/utils/mask_test.go b/utils/mask_test.go index b3450ee7..40263aae 100644 --- a/utils/mask_test.go +++ b/utils/mask_test.go @@ -16,8 +16,9 @@ func TestMaskSecret(t *testing.T) { expected string }{ {"empty", "", ""}, - {"single_char", "x", "x"}, - {"two_chars", "ab", "ab"}, + {"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"}, From ecdc8465b0f911b296ae6b9d792503a4501b771d Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Fri, 10 Apr 2026 13:35:20 +0000 Subject: [PATCH 3/5] refactor: unify redactHeaderValue and MaskSecret funcs --- intercept/apidump/apidump.go | 3 +- intercept/apidump/apidump_test.go | 6 ++-- intercept/apidump/headers.go | 14 -------- intercept/apidump/headers_test.go | 56 +------------------------------ 4 files changed, 6 insertions(+), 73 deletions(-) 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..043ba94a 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, reveal=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, reveal=2, 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", From 9cc7021a41265d4d3a1df4d6c1c621bda92bac2d Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Fri, 10 Apr 2026 13:40:11 +0000 Subject: [PATCH 4/5] refactor: minor fix --- intercept/apidump/apidump_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intercept/apidump/apidump_test.go b/intercept/apidump/apidump_test.go index 043ba94a..2b8d3d15 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: se...23") // "session=abc123" is 14 chars, reveal=2 + 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") From c50b1dbb992b26afe2042ed491be9df1f57c61bb Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Fri, 10 Apr 2026 13:41:52 +0000 Subject: [PATCH 5/5] refactor: minor fix --- intercept/apidump/apidump_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intercept/apidump/apidump_test.go b/intercept/apidump/apidump_test.go index 2b8d3d15..e2600850 100644 --- a/intercept/apidump/apidump_test.go +++ b/intercept/apidump/apidump_test.go @@ -133,7 +133,7 @@ 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, reveal=2, first 2 = "Be", last 2 = "i\"" + // "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