Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/utils/providerProfile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1588,6 +1588,7 @@ test('maskSecretForDisplay preserves only a short prefix and suffix', () => {
test('redactSecretValueForDisplay masks poisoned display fields that equal configured secrets', () => {
const apiKey = 'sk-secret-12345678'
const authHeaderValue = 'hicap-header-secret'
const routeApiKey = 'gsk-route-secret-value'

assert.equal(
redactSecretValueForDisplay(apiKey, { OPENAI_API_KEY: apiKey }),
Expand All @@ -1599,10 +1600,49 @@ test('redactSecretValueForDisplay masks poisoned display fields that equal confi
}),
'hic...ret',
)
assert.equal(
redactSecretValueForDisplay(routeApiKey, { GROQ_API_KEY: routeApiKey }),
'gsk...lue',
)
assert.equal(
redactSecretValueForDisplay('gpt-4o', { OPENAI_API_KEY: apiKey }),
'gpt-4o',
)
assert.equal(
redactSecretValueForDisplay('gpt-4o', { OPENAI_MODEL: 'gpt-4o' }),
'gpt-4o',
)
})

test('redactSecretValueForDisplay collects common secret env suffixes', () => {
const secretEnvCases = [
['ROUTE_API_KEY', 'route-api-secret-value'],
['ROUTE_AUTH_HEADER_VALUE', 'route-auth-header-secret'],
['SERVICE_PASSWORD', 'database-password-secret'],
['SERVICE_SECRET', 'service-secret-value'],
['AWS_SECRET_ACCESS_KEY', 'aws-secret-access-value'],
['OAUTH_SECRET_KEY', 'oauth-secret-key-value'],
['GITHUB_TOKEN', 'github-token-secret'],
] as const

for (const [key, value] of secretEnvCases) {
const source = { [key]: value }

assert.equal(
redactSecretValueForDisplay(value, source),
maskSecretForDisplay(value),
)
assert.equal(sanitizeProviderConfigValue(value, source), undefined)
}

assert.equal(
redactSecretValueForDisplay('gpt-4o', { OPENAI_MODEL: 'gpt-4o' }),
'gpt-4o',
)
assert.equal(
sanitizeProviderConfigValue('gpt-4o', { OPENAI_MODEL: 'gpt-4o' }),
'gpt-4o',
)
})

test('sanitizeProviderConfigValue drops secret-like poisoned values', () => {
Expand Down
8 changes: 6 additions & 2 deletions src/utils/providerSecrets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
const FAKE_OPENAI_KEY = 'sk-fake-openai-1234567890abcdef'
const FAKE_GEMINI_KEY = 'AIzaSyFAKEGEMINIkey1234567890abcdefghijklmnopqr'
const FAKE_GITHUB_PAT = 'ghp_FAKEgithubPat0123456789abcdefghij'
const FAKE_GITHUB_USER_TOKEN = 'ghu_1234567890abcdef1234567890abcdef1234'
const FAKE_LONG_OPAQUE = 'live-pr-1234567890abcdefABCDEF1234567890abcdef'
const FAKE_JWT_TOKEN = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'

Expand Down Expand Up @@ -179,6 +180,7 @@ describe('redactSecretValueForDisplay', () => {
expect(redactSecretValueForDisplay('sk-ant-fakeAnthropicToken-1234567890')).toBe('sk-...890')
expect(redactSecretValueForDisplay('AIzaSyFAKEGEMINIkey1234567890abcdefghijklmnopqr')).toBe('AIz...pqr')
expect(redactSecretValueForDisplay(FAKE_GITHUB_PAT)).toBe('ghp...hij')
expect(redactSecretValueForDisplay(FAKE_GITHUB_USER_TOKEN)).toBe('ghu...234')
expect(redactSecretValueForDisplay(FAKE_LONG_OPAQUE)).toBe('liv...def')
expect(redactSecretValueForDisplay(FAKE_JWT_TOKEN)).toBe('eyJ...w5c')
})
Expand Down Expand Up @@ -242,11 +244,12 @@ describe('redactSecretSubstringsForDisplay', () => {
const leakedKey = 'sk-liveLeakToken1234567890ABCdef'

const redacted = redactSecretSubstringsForDisplay(
`Invalid API key: ${leakedKey}`,
`Invalid API key: ${leakedKey}; GitHub token: ${FAKE_GITHUB_USER_TOKEN}`,
)

expect(redacted).toBe('Invalid API key: sk-...def')
expect(redacted).toBe('Invalid API key: sk-...def; GitHub token: ghu...234')
expect(redacted).not.toContain(leakedKey)
expect(redacted).not.toContain(FAKE_GITHUB_USER_TOKEN)
})

test('redacts JWT-shaped values embedded in longer messages', () => {
Expand Down Expand Up @@ -285,6 +288,7 @@ describe('sanitizeProviderConfigValue', () => {
expect(sanitizeProviderConfigValue('sk-ant-looks-like-a-key-1234567890')).toBeUndefined()
expect(sanitizeProviderConfigValue('AIzaSySomeGeminiKey1234567890abcdefghijklmnopqr')).toBeUndefined()
expect(sanitizeProviderConfigValue(FAKE_GITHUB_PAT)).toBeUndefined()
expect(sanitizeProviderConfigValue(FAKE_GITHUB_USER_TOKEN)).toBeUndefined()
expect(sanitizeProviderConfigValue(FAKE_JWT_TOKEN)).toBeUndefined()
})

Expand Down
29 changes: 26 additions & 3 deletions src/utils/providerSecrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,14 @@ const SECRET_PREFIX_PATTERNS = [
/^AIza/,
/^ghp_/,
/^gho_/,
/^ghu_/,
/^ghs_/,
/^ghr_/,
/^github_pat_/,
]

const SECRET_PREFIX_SUBSTRING_PATTERN =
/(?:sk-ant-|sk-|AIza|ghp_|gho_|ghs_|ghr_|github_pat_)[A-Za-z0-9._-]{8,}/g
/(?:sk-ant-|sk-|AIza|ghp_|gho_|ghu_|ghs_|ghr_|github_pat_)[A-Za-z0-9._-]{8,}/g
const JWT_SUBSTRING_PATTERN =
/\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g

Expand Down Expand Up @@ -152,16 +153,38 @@ function hasLowerUpperDigit(value: string): boolean {
return hasLower && hasUpper && hasDigit
}

// Redaction sources may be full process env objects, so also collect values
// from generic credential-bearing suffixes. The descriptor registry covers
// known providers; this defensive path covers custom routes and cloud/database
// auth variables that can still be surfaced through status/config displays.
function isSecretEnvKey(
key: string,
knownKeys: ReadonlySet<string>,
): boolean {
return (
knownKeys.has(key) ||
key.endsWith('_API_KEY') ||
key.endsWith('_AUTH_HEADER_VALUE') ||
key.endsWith('_PASSWORD') ||
key.endsWith('_SECRET') ||
key.endsWith('_SECRET_ACCESS_KEY') ||
key.endsWith('_SECRET_KEY') ||
key.endsWith('_TOKEN')
)
}

function collectSecretValues(
sources: Array<SecretValueSource | null | undefined>,
): string[] {
const knownKeys = getKnownProviderSecretEnvKeys()
const knownKeys = new Set(getKnownProviderSecretEnvKeys())
const values = new Set<string>()

for (const source of sources) {
if (!source) continue

for (const key of knownKeys) {
for (const key of Object.keys(source)) {
if (!isSecretEnvKey(key, knownKeys)) continue

const value = sanitizeApiKey(source[key])?.trim()
if (value) {
values.add(value)
Expand Down
Loading