feat: make sure creds are always masked (#24241)

## Summary  
Adds a `sanitizeCredentialHint` safety check in the db-to-SDK conversion
layer to ensure credential hints are always masked before being exposed
in the API. Also adds `credential_kind` and `credential_hint` assertions
to the session threads API test.
This commit is contained in:
Yevhenii Shcherbina
2026-04-13 10:14:38 -04:00
committed by GitHub
parent 4854f33678
commit b78eba9f9d
5 changed files with 57 additions and 8 deletions
+20 -1
View File
@@ -1241,7 +1241,7 @@ func buildAIBridgeThread(
thread.Model = rootIntc.Model
thread.Provider = rootIntc.Provider
thread.CredentialKind = string(rootIntc.CredentialKind)
thread.CredentialHint = rootIntc.CredentialHint
thread.CredentialHint = sanitizeCredentialHint(rootIntc.CredentialHint)
// Get first user prompt from root interception.
// A thread can only have one prompt, by definition, since we currently
// only store the last prompt observed in an interception.
@@ -1407,6 +1407,25 @@ func InvalidatedPresets(invalidatedPresets []database.UpdatePresetsLastInvalidat
return presets
}
// sanitizeCredentialHint ensures the hint looks masked before exposing
// it in the API. The aibridge library uses "..." as the masking
// delimiter (e.g. "sk-a...efgh"), so we check for its presence. If
// the hint doesn't contain "..." or exceeds the max length, it's
// replaced with "..." to prevent leaking raw secrets.
func sanitizeCredentialHint(hint string) string {
// Matches the VARCHAR(15) DB constraint.
const maxCredentialHintLength = 15
if hint == "" {
return ""
}
if len(hint) > maxCredentialHintLength || !strings.Contains(hint, "...") {
return "..."
}
return hint
}
func jsonOrEmptyMap(rawMessage pqtype.NullRawMessage) map[string]any {
var m map[string]any
if !rawMessage.Valid {
@@ -306,3 +306,29 @@ func TestAggregateTokenUsage(t *testing.T) {
require.Empty(t, result.Metadata)
})
}
func TestSanitizeCredentialHint(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expected string
}{
{"valid_short", "s...t", "s...t"},
{"valid_long", "sk-a...efgh", "sk-a...efgh"},
{"valid_only_dots", "...", "..."},
{"empty", "", ""},
{"short_unmasked_secret", "abc12", "..."},
{"missing_dots", "sk-abcdefgh", "..."},
{"too_long", "sk-a...efghijklmn", "..."},
{"raw_secret", "sk-proj-abc123xyz789", "..."},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tc.expected, sanitizeCredentialHint(tc.input))
})
}
}