T09 · Insecure Skill Coding Practices
- Location
scripts/wakapi-daily-summary.mjs:43- Finding
Wakapi API Key Can Be Sent to an Untrusted or Unencrypted Endpoint
- Content
View full analysis
Vulnerability Details
File Location:
scripts/wakapi-daily-summary.mjs, lines 19–31, 43–52, and 187–205
Vulnerability Type: Unrestricted credential transmission
Risk Level: MediumThe Skill accepts an arbitrary
WAKAPI_URLand sends the Wakapi API key to that destination in an HTTP Basic authorization header. The URL is not validated to ensure that it uses HTTPS or identifies a trusted Wakapi host.js function getConfig() { const WAKAPI_URL = process.env.WAKAPI_URL; const WAKAPI_API_KEY = process.env.WAKAPI_API_KEY; const OUT_DIR = process.env.WAKAPI_OUT_DIR; const TOP_N_PROJECTS = Number(process.env.WAKAPI_TOP_N_PROJECTS || 10); const TOP_N_LANGUAGES = Number(process.env.WAKAPI_TOP_N_LANGUAGES || 10); if (!WAKAPI_URL || !WAKAPI_API_KEY || !OUT_DIR) { console.error('Missing required env vars: WAKAPI_URL, WAKAPI_API_KEY, WAKAPI_OUT_DIR'); process.exit(2); } return { WAKAPI_URL, WAKAPI_API_KEY, OUT_DIR, TOP_N_PROJECTS, TOP_N_LANGUAGES }; }js async function httpJson(url, apiKey) { const headers = { 'Accept': 'application/json', 'Authorization': `Basic ${b64(apiKey)}`, }; const res = await fetch(url, { headers }); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`HTTP ${res.status} ${res.statusText} for ${url}\n${text.slice(0, 500)}`); } return res.json(); }js const base = WAKAPI_URL.replace(/\/$/, ''); const statusbarUrl = `${base}/api/v1/users/current/statusbar/today`; const summariesUrl = `${base}/api/v1/users/current/summaries?range=today`; let statusbar; try { statusbar = await httpJson(statusbarUrl, WAKAPI_API_KEY); } catch (e) { console.error(`[wakapi-sync] Failed statusbar/today: ${e.message}`); throw e; } let { totalSeconds, projects, languages } = extractTopFromStatusbarToday(statusbar); // If statusbar doesn't con ...[truncated 2252 chars]- Remediation
View remediation
Remediation Suggestions
- Parse
WAKAPI_URLwithnew URL()before constructing any request. - Require the
https:scheme by default and reject unsupported schemes, embedded user information, fragments, and malformed URLs. - If plaintext HTTP is needed for a loopback-only deployment, require a clearly named explicit opt-in and restrict it to verified loopback addresses.
- Provide an optional hostname allowlist or configured trusted-origin check for managed deployments.
- Build endpoints with the
URLAPI rather than string concatenation. - Disable automatic redirects or validate every redirect target against the original trusted origin before following it.
- Use a minimally privileged, read-only Wakapi API key where the server supports scoped credentials.
- Add tests confirming rejection of
http://, non-HTTP schemes, embedded credentials, untrusted hosts, and cross-origin redirects.
Example hardening:
js function validateWakapiBase(rawUrl) { const url = new URL(rawUrl); if (url.protocol !== 'https:') { throw new Error('WAKAPI_URL must use HTTPS'); } if (url.username || url.password) { throw new Error('WAKAPI_URL must not contain embedded credentials'); } if (url.search || url.hash) { throw new Error('WAKAPI_URL must not contain a query string or fragment'); } return url; }- Parse
