Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -1535,6 +1535,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 @@ -1546,10 +1547,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
23 changes: 21 additions & 2 deletions src/utils/providerSecrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const SECRET_ENV_KEYS = [
] as const

export type SecretValueSource = Partial<
Record<(typeof SECRET_ENV_KEYS)[number], string | undefined>
Record<string, string | undefined>
>

export function sanitizeApiKey(
Expand All @@ -35,6 +35,23 @@ function looksLikeSecretValue(value: string): boolean {
return false
}

// Redaction sources may be full process env objects, so only collect values
// from keys that look credential-bearing. The suffix list intentionally covers
// route API keys plus common auth tokens and cloud/database secrets; masking too
// much is safer than leaking a credential in status or provider displays.
function isSecretEnvKey(key: string): boolean {
return (
SECRET_ENV_KEYS.includes(key as (typeof SECRET_ENV_KEYS)[number]) ||
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[] {
Expand All @@ -43,7 +60,9 @@ function collectSecretValues(
for (const source of sources) {
if (!source) continue

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

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