T09 · Insecure Skill Coding Practices
- Location
scripts/finance_lite/brief.mjs:232- Finding
Finnhub API Key Disclosure Through Error Logging
- Content
View full analysis
Vulnerability Details
File Location:
scripts/finance_lite/brief.mjs, lines 232–234, 414–435, and 867–869
Vulnerability Type: Sensitive credential exposure through exception messages
Risk Level: MediumVulnerable Code
js async function fetchJson(url) { const res = await fetch(url, { headers: { accept: "application/json", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36", }, }); const ct = (res.headers.get("content-type") || "").toLowerCase(); const text = await res.text(); if (!res.ok) throw new Error(`HTTP ${res.status} ${url} :: ${text.slice(0, 120)}`); if (!ct.includes("application/json")) { throw new Error(`Non-JSON response for ${url} (content-type=${ct}) :: ${text.slice(0, 120)}`); } return JSON.parse(text); }js async function quote(symbol) { return await cached(`quote_${symbol}`, 60, async () => { const url = `${FINNHUB}/quote?symbol=${encodeURIComponent(symbol)}&token=${encodeURIComponent(FINNHUB_KEY)}`; return fetchJson(url); }); } async function marketNews() { return await cached("market_news_general", 30 * 60, async () => { const url = `${FINNHUB}/news?category=general&token=${encodeURIComponent(FINNHUB_KEY)}`; return fetchJson(url); }); } async function companyNews(symbol) { const to = new Date(); const from = new Date(Date.now() - 7 * 86400 * 1000); return await cached(`company_news_${symbol}_${ymd(from)}_${ymd(to)}`, 30 * 60, async () => { const url = `${FINNHUB}/company-news` + `?symbol=${encodeURIComponent(symbol)}` + `&from=${encodeURIComponent(ymd(from))}` + `&to=${encodeURIComponent(ymd(to))}` + `&token=${encodeURIComponent(FINNHUB_KEY)}`; return fetchJson(url); }); }js main().catch((e) => { console.error("Finance Lite error:", e?.message ?? e); process.exit(1); });Technical Analysis
The Fin ...[truncated 2051 chars]
- Remediation
View remediation
Remediation Suggestions
- Never place raw credential-bearing URLs in exception messages. Log only a sanitized endpoint name, HTTP method, status code, and non-sensitive diagnostic information.
- Redact sensitive query parameters before any URL is logged:
js function sanitizeUrl(rawUrl) { const parsed = new URL(rawUrl); for (const key of ["token", "api_key", "key", "access_token"]) { if (parsed.searchParams.has(key)) parsed.searchParams.set(key, "[REDACTED]"); } return parsed.toString(); }- Use the sanitizer in all failure branches:
js if (!res.ok) { throw new Error( `HTTP ${res.status} ${sanitizeUrl(url)} :: ${text.slice(0, 120)}` ); } if (!ct.includes("application/json")) { throw new Error( `Non-JSON response for ${sanitizeUrl(url)} (content-type=${ct})` ); }- Avoid logging response bodies unless required. Upstream error bodies can also contain sensitive or attacker-controlled content; restrict them to a small sanitized diagnostic field.
- Prefer an authorization header instead of a query parameter if the provider supports it, because query strings are commonly retained by proxies and logs.
- Add automated tests that use sentinel credentials and assert that thrown errors and stderr never contain those values.
- Rotate the Finnhub API key if existing logs or transcripts may already contain a failed request.
- Validate
FINNHUB_API_KEYonly for commands that perform Finnhub requests. Local-only operations such aslistandremovedo not require network credentials and should remain available without loading or validating the key.
