T09 · Insecure Skill Coding Practices
- Location
src/ai/index.ts:13- Finding
Unvalidated custom AI endpoint can receive sensitive note content, search queries, and API credentials
- Content
View full analysis
Vulnerability Details
File Location:
src/ai/index.ts:13-16, 127-137, 211-221, 283-298, 332-347
Vulnerability Type: Unrestricted sensitive-data transmission to a configurable endpoint
Risk Level: MediumVulnerable Code
The destination and bearer credential are loaded directly from environment variables without validating the URL scheme, hostname, or resolved network address:
ts const OPENAI_API_KEY = process.env.OPENAI_API_KEY || ''; const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'; const EMBEDDING_MODEL = process.env.EMBEDDING_MODEL || 'text-embedding-3-small'; const CHAT_MODEL = process.env.CHAT_MODEL || 'gpt-4o-mini';Individual note content is sent for embedding:
ts const response = await fetch(`${OPENAI_BASE_URL}/embeddings`, { method: 'POST', headers: { 'Authorization': `Bearer ${OPENAI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: EMBEDDING_MODEL, input: text.slice(0, 8000), }), });Batch input is transmitted in the same manner:
ts const response = await fetch(`${OPENAI_BASE_URL}/embeddings`, { method: 'POST', headers: { 'Authorization': `Bearer ${OPENAI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: EMBEDDING_MODEL, input: texts.map(t => t.slice(0, 8000)), }), });Note contents are also sent for automatic summaries and tags:
ts const response = await fetch(`${OPENAI_BASE_URL}/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${OPENAI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: CHAT_MODEL, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: content.slice(0, 4000) }, ], temperature: 0.3, response_format: { type: 'json_object' }, }), });Search queries are transmitted to the same configurable endpoint:
ts con ...[truncated 3623 chars]- Remediation
View remediation
Remediation Suggestions
- Parse the configured endpoint with
new URL()before making any request. - Require
https:and reject cleartext HTTP except for an explicitly enabled local-development mode. - Use an allowlist of approved provider hostnames, with
api.openai.comas the secure default. - Resolve the hostname and reject loopback, link-local, private, multicast, and cloud metadata address ranges. Repeat validation after redirects or disable redirects.
- Require explicit configuration and a visible warning before allowing a non-default endpoint.
- Do not reuse
OPENAI_API_KEYautomatically for arbitrary OpenAI-compatible endpoints. Use endpoint-specific credential variables or credential records bound to an approved origin. - Display the destination and data categories that will be transmitted, and obtain explicit consent before enabling cloud processing.
- Provide a clear local-only mode that prevents all cloud requests even when an API key exists.
- Consider separate controls for embedding, enhancement, and search-query expansion so users can minimize transmitted data.
- Add automated tests covering HTTP rejection, hostile domains, loopback addresses, private IP addresses, redirects to prohibited destinations, malformed URLs, and endpoint-specific credential binding.
A minimal validation pattern should resemble:
ts function validateAIEndpoint(rawBaseUrl: string): URL { const endpoint = new URL(rawBaseUrl); if (endpoint.protocol !== 'https:') { throw new Error('AI endpoints must use HTTPS'); } const allowedHosts = new Set(['api.openai.com']); if (!allowedHosts.has(endpoint.hostname)) { throw new Error('Unapproved AI endpoint'); } return endpoint; }For deliberately supported third-party providers, maintain a separate allowlisted provider configuration and require credentials scoped specifically to that provider.
- Parse the configured endpoint with
