T09 · Insecure Skill Coding Practices
- Location
scripts/lnurl_auth.js:343- Finding
Unrestricted outbound HTTP requests enable server-side request forgery
- Content
View full analysis
{ let url; try { url = new URL(urlString); } catch { reject(new Error(`Invalid URL: ${urlString}`)); return; } if (!['http:', 'https:'].includes(url.protocol)) { reject(new Error(`Unsupported URL protocol: ${url.protocol}`)); return; } if (redirects > MAX_REDIRECTS) { reject(new Error(`Too many redirects (${redirects})`)); return; } const client = url.protocol === 'https:' ? https : http; const request = client.get(url, { timeout, headers: { 'User-Agent': USER_AGENT } }, (response) => { if ([301, 302, 307, 308].includes(response.statusCode) && response.headers.location) { response.resume(); resolve(httpGet(new URL(response.headers.location, url).toString(), timeout, redirects + 1)); return; } let body = ''; response.setEncoding('utf8'); response.on('data', (chunk) => { body += chunk; }); response.on('end', () => resolve({ status: response.statusCode, body })); }); request.on('timeout', () => request.destroy(new Error(`Request timed out after ${timeout}ms`))); request.on('error', reject); }); } ``` The untrusted URLs reach this function through the decoded LNURL, challenge-provided callback, command-line callback override, and authentication callback: ```javascript const serviceUrl = decodeLnurl(lnurl); const parsed = new URL(serviceUrl); if (!parsed.hostname) throw new Error('Decoded LNURL has no service hostname'); if (!k1) { log('No k1 in URL; fetching challenge...'); const challenge = await httpGet(serviceUrl, timeout); // ... } const callback = new URL(opts.callback || callbackFromChallenge || se ...[truncated 2678 chars]- Remediation
View remediation
