T09 · Insecure Skill Coding Practices
Error
- Location
- bb-sites/gumtree/listing.js:66
- Finding
- Unrestricted Listing URL Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `bb-sites/gumtree/listing.js`, lines 66–75 **Vulnerability Type**: Server-Side Request Forgery (SSRF) through insufficient URL validation **Risk Level**: High ### Vulnerable Code ```javascript if (!args.url) return { error: 'Missing argument: url', hint: 'e.g. bb-browser site gumtree/listing "https://www.gumtree.com/p/.../ID"' }; let path = String(args.url).trim(); if (!path.startsWith('http')) { if (!path.startsWith('/')) path = '/' + path; path = 'https://www.gumtree.com' + path; } const resp = await fetch(path, { ``` ### Technical Analysis The listing adapter accepts a caller-controlled URL. Values beginning with `http` are passed directly to `fetch()` without parsing or validating the scheme, hostname, port, resolved IP address, or redirect destination. Although the adapter metadata declares `www.gumtree.com` as its domain, this declaration is not enforced by the code. The `startsWith('http')` condition is only a string-prefix check and does not establish that the destination belongs to Gumtree. Consequently, an attacker may provide URLs targeting: - Loopback services such as `http://127.0.0.1/...` - Private network services - Link-local or cloud instance metadata endpoints - Internal administrative interfaces - Attacker-controlled hosts that redirect to internal destinations The default redirect behavior of `fetch()` creates an additional bypass path because the final destination is not validated before response processing. The response body is read and parsed for JSON-LD and Open Graph content, potentially returning information obtained from a destination that the caller could not access directly. ### Attack Path 1. An attacker invokes `gumtree/listing` with an HTTP or HTTPS URL under the attacker’s control, or directly supplies an internal-service URL. 2. The URL begins with `http`, so the adapter does not convert it into a Gumtree URL. 3. The unvalidated value is passed to `fetch()`. ...[truncated 1127 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Parse the input using `new URL()` rather than relying on string prefixes. 2. Require the `https:` protocol. 3. Permit only the exact hostname `www.gumtree.com`, or a narrowly defined allowlist of required Gumtree hostnames. 4. Reject URLs containing credentials, unexpected ports, malformed hostnames, or unsupported schemes. 5. Disable automatic redirects and validate every redirect destination before following it. 6. Resolve destination hostnames and reject loopback, private, link-local, multicast, reserved, and otherwise non-public IP ranges for both IPv4 and IPv6. 7. Protect against DNS rebinding by ensuring that validation and connection use the same resolved address. 8. Apply request timeouts, response-size limits, and conservative content-type checks. 9. Prefer accepting a Gumtree listing path or listing identifier rather than an arbitrary absolute URL. A minimum hostname validation pattern is: ```javascript const candidate = new URL(String(args.url), 'https://www.gumtree.com'); if ( candidate.protocol !== 'https:' || candidate.hostname !== 'www.gumtree.com' || candidate.username || candidate.password || candidate.port ) { return { error: 'Only HTTPS URLs on www.gumtree.com are allowed' }; } const resp = await fetch(candidate.href, { redirect: 'manual', headers: { 'User-Agent': '...', Accept: 'text/html,application/xhtml+xml', 'Accept-Language': 'en-GB,en;q=0.9', }, }); ``` Production hardening should additionally validate redirect targets and resolved IP addresses. ]]>
