mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
committed by
GitHub
parent
4854f33678
commit
b78eba9f9d
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user