T09 · Insecure Skill Coding Practices
- Location
scripts/process.cjs:50- Finding
Arbitrary HTTPS Requests Caused by Inadequate t.co Host Validation
- Content
View full analysis
{ if (!url.includes('t.co/')) { resolve(url); return; } const req = https.get(url, { method: 'HEAD' }, (res) => { resolve(res.headers.location || url); }); req.on('error', () => resolve(url)); req.setTimeout(10000, () => { req.destroy(); resolve(url); }); }); } ``` ### Technical Analysis The function is intended to expand links hosted by the `t.co` URL-shortening service. However, it verifies the URL with a substring search: ```javascript url.includes('t.co/') ``` This does not establish that the parsed hostname is actually `t.co`. The string can appear in a path, user-information component, query string, or attacker-controlled hostname. For example, the following URL passes the check even though its host is not `t.co`: ```text https://internal.example/t.co/probe ``` The function then passes the complete untrusted URL to `https.get()`, causing the host running the Skill to issue a HEAD request to the attacker-selected HTTPS destination. Because response bodies are not read and redirects are not automatically followed by this code, the exposure is narrower than a fully featured SSRF vulnerability. Nevertheless, it provides a network-probing primitive and exceeds the minimum privilege needed to contact only the legitimate `t.co` service. ### Attack Path 1. An attacker causes a crafted URL to appear in an X bookmark or writes a crafted bookmark object into the pending state file. 2. `extractUrl()` extracts the attacker-controlled URL. 3. The URL contains the substring `t.co/`, but its actual hostname points to an internal or otherwise attacker-selected service. 4. `expandUrl()` accepts the URL because it uses ...[truncated 893 chars]- Remediation
View remediation
{ let parsed; try { parsed = new URL(url); } catch { resolve(url); return; } if ( parsed.protocol !== 'https:' || parsed.hostname.toLowerCase() !== 't.co' || parsed.username || parsed.password ) { resolve(url); return; } const req = https.get(parsed, { method: 'HEAD' }, (res) => { const location = res.headers.location; if (!location) { resolve(url); return; } try { resolve(new URL(location, parsed).toString()); } catch { resolve(url); } }); req.on('error', () => resolve(url)); req.setTimeout(10000, () => { req.destroy(); resolve(url); }); }); } ``` Additional hardening should include: 1. Resolve the destination host and reject loopback, private, link-local, multicast, and reserved address ranges. 2. Revalidate the remote address after DNS resolution to mitigate DNS rebinding. 3. Reject embedded credentials and non-HTTPS protocols. 4. Limit redirects and validate every redirect destination before contacting it. 5. Add tests for deceptive values such as: - `https://example.com/t.co/test` - `https://t.co.attacker.example/test` - `https://t.co@attacker.example/test` - private and loopback destinations 6. Prefer a dedicated URL-expansion implementation with a strict outbound-host policy. ]]>
