T09 · Insecure Skill Coding Practices
Error
- Location
- src/observer.ts:84
- Finding
- Provider-Agnostic API Key Fallback Can Disclose Credentials to the Wrong LLM Provider<![CDATA[ ## Vulnerability Details **File Location**: `src/observer.ts:84-129` **Vulnerability Type**: Cross-provider credential disclosure caused by insecure credential selection **Risk Level**: High ### Vulnerable Code ```typescript 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 — key in header, not URL (avoids log exposure) const res = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/${this.config.model || 'gemini-2.0-flash'}:generateContent`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-goog-api-key': key }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }), } ); const data = await res.json() as any; return data.candidates?.[0]?.content?.parts?.[0]?.text || ''; } ``` ### Technical Ana ...[truncated 2453 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions Bind environment-variable selection strictly to the configured provider and reject missing credentials before issuing a request: ```typescript private getApiKey(): string { const environmentKey = this.config.provider === 'openai' ? process.env.OPENAI_API_KEY : this.config.provider === 'anthropic' ? process.env.ANTHROPIC_API_KEY : process.env.GEMINI_API_KEY; const key = this.config.apiKey || environmentKey; if (!key) { throw new Error(`Missing API key for provider: ${this.config.provider}`); } return key; } ``` Additional hardening measures: 1. Never fall back from one provider's environment variable to another provider's variable. 2. Document and support `GEMINI_API_KEY`. 3. Validate `provider` at runtime, particularly when configuration originates from untyped JavaScript or external data. 4. Ensure errors and diagnostic logs never include request headers or credential values. 5. Add tests confirming that each provider can access only its corresponding environment variable. 6. Add negative tests proving that a provider request is not sent when only another provider's key is available. 7. Prefer separately scoped, low-quota credentials for each provider to limit the impact of accidental disclosure. ]]>
