T09 · Insecure Skill Coding Practices
Warning
- Location
- lib/api.js:25
- Finding
- Unrestricted HTTPS Requests and Unvalidated Redirect Destinations<![CDATA[ ## Vulnerability Details **File Location**: `lib/api.js`, lines 25–65; exported at lines 246–255 **Vulnerability Type**: Server-Side Request Forgery (SSRF) and unbounded network response handling **Risk Level**: Medium ### Vulnerable Code ```javascript function httpGet(url, redirects = 0) { return new Promise((resolve, reject) => { if (redirects > 5) { reject(new Error('Too many redirects')); return; } const parsedUrl = new URL(url); const options = { hostname: parsedUrl.hostname, path: parsedUrl.pathname + parsedUrl.search, method: 'GET', headers: { 'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'User-Agent': 'PMC-Harvest/1.0' } }; https.get(options, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { const location = res.headers.location; const redirectUrl = location.startsWith('http') ? location : new URL(location, parsedUrl.origin).href; return httpGet(redirectUrl, redirects + 1).then(resolve).catch(reject); } let stream = res; const encoding = res.headers['content-encoding']; if (encoding === 'gzip') { stream = res.pipe(zlib.createGunzip()); } else if (encoding === 'deflate') { stream = res.pipe(zlib.createInflate()); } let data = ''; stream.on('data', chunk => data += chunk); stream.on('end', () => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(data); } else { reject(new Error(`HTTP ${res.statusCode}`)); } }); stream.on('error', reject); }).on('error', reject); }); } ``` The unrestricted helper is also exposed as part of the public module API: ```javascript module.exports = { searchPMC, getSummaries, fetchFullText, fetchAbstract, parseJATS, harvestJournals, httpGet }; ``` ### Technical Analysis Th ...[truncated 2821 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. **Remove the generic helper from the public API** - Do not export `httpGet` unless arbitrary outbound requests are an explicit, documented requirement. - Expose only task-specific methods such as `searchPMC` and `fetchFullText`. 2. **Enforce an exact destination allowlist** - Permit only `https:`. - Permit only the documented hosts: - `eutils.ncbi.nlm.nih.gov` - `pmc.ncbi.nlm.nih.gov` - Reject URL credentials, unexpected ports, IP literals, malformed hostnames, and hostnames that merely end with an allowed string. 3. **Validate every redirect** - Resolve relative redirects with `new URL(location, currentUrl)`. - Apply the same scheme, hostname, port, and address validation before following each redirect. - Prefer rejecting cross-origin redirects unless they are explicitly required and allowlisted. 4. **Restrict private and special-purpose addresses** - Resolve the destination hostname and reject loopback, link-local, private, multicast, and other special-use IPv4 and IPv6 ranges. - Account for DNS rebinding by verifying the address actually used for the connection. 5. **Bound resource consumption** - Add connection and response timeouts. - Abort the request after a fixed compressed or decompressed byte limit. - Handle decompression errors and oversized decompressed output. - Destroy the response stream before following a redirect. 6. **Construct requests with the validated URL** - Pass the validated URL object to `https.get` or explicitly include the validated protocol and port. - Revalidate immediately before each outbound connection. ]]>
