T09 · Insecure Skill Coding Practices
Error
- Location
- src/observer.ts:67
- Finding
- Provider-Agnostic API-Key Fallback Can Disclose Credentials to the Wrong LLM Provider## Vulnerability Details **File Location**: `src/observer.ts`, lines 67–108 **Vulnerability Type**: Cross-provider credential disclosure caused by insecure credential selection **Risk Level**: High ### Vulnerable 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
- ## Remediation Suggestions 1. 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 || ''; } } ``` 2. 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}`); } ``` 3. Never fall back to another provider's environment variable. 4. 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. 5. Validate the provider and model combination so that unsupported or unexpected configuration fails before any sensitive data is transmitted. 6. 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. 7. Document `GEMINI_API_KEY` alongside the other supported environment variables and explain that conversation content is sent to the selected provider.
