T09 · Insecure Skill Coding Practices
- Location
src/observer.ts:67- Finding
Provider-Agnostic API-Key Fallback Can Disclose Credentials to the Wrong LLM Provider
- Content
View full analysis
Vulnerability Details
File Location:
src/observer.ts, lines 67–108
Vulnerability Type: Cross-provider credential disclosure caused by insecure credential selection
Risk Level: HighVulnerable Code
ts private async callLLM(prompt: string): Promise<string> { const key = this.config.apiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || ''; if (this.config.provider === 'openai') { const res = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` }, body: JSON.stringify({ model: this.config.model || 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }], max_tokens: 2000, }), }); const data = await res.json() as any; return data.choices?.[0]?.message?.content || ''; } if (this.config.provider === 'anthropic') { const res = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': key, 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: this.config.model || 'claude-3-5-haiku-20241022', max_tokens: 2000, messages: [{ role: 'user', content: prompt }], }), }); const data = await res.json() as any; return data.content?.[0]?.text || ''; } // Gemini const res = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/${this.config.model || 'gemini-2.0-flash'}:generateContent?key=${key}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }), } ); const data = await res.json() as any; return data.candidates?.[0]?.content?.parts?.[0]?.text || ''; }Technical Analysis
The credential-selection logic is independent of the confi ...[truncated 3177 chars]
- Remediation
View remediation
Remediation Suggestions
-
Select credentials strictly according to the configured provider:
ts private getApiKey(): string { if (this.config.apiKey) return this.config.apiKey; switch (this.config.provider) { case 'openai': return process.env.OPENAI_API_KEY || ''; case 'anthropic': return process.env.ANTHROPIC_API_KEY || ''; case 'gemini': return process.env.GEMINI_API_KEY || ''; } } -
Reject missing credentials before constructing or sending a request:
ts const key = this.getApiKey(); if (!key) { throw new Error(`Missing API key for provider: ${this.config.provider}`); } -
Never fall back to another provider's environment variable.
-
Use an authentication header for Gemini if supported by the selected API interface. If the API requires a query parameter, ensure request URLs are redacted from logs, telemetry, and exception messages.
-
Validate the provider and model combination so that unsupported or unexpected configuration fails before any sensitive data is transmitted.
-
Add mocked unit tests that verify:
- OpenAI receives only an explicit key or
OPENAI_API_KEY. - Anthropic receives only an explicit key or
ANTHROPIC_API_KEY. - Gemini receives only an explicit key or
GEMINI_API_KEY. - Missing provider-specific credentials cause a local failure without a network request.
- Test runs never contact real provider endpoints.
- OpenAI receives only an explicit key or
-
Document
GEMINI_API_KEYalongside the other supported environment variables and explain that conversation content is sent to the selected provider.
-
