T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/gmc-store-audit.mjs:20
- Finding
- Incomplete SSRF Protection Permits Access to Internal and Link-Local Network Resources<![CDATA[ ## Vulnerability Details **File Location**: `scripts/gmc-store-audit.mjs:20-67, 425-432`; `scripts/gmc-product-audit.mjs:23-42, 78-91` **Vulnerability Type**: Server-Side Request Forgery (SSRF) caused by incomplete address validation and unchecked redirects **Risk Level**: High ### Vulnerable Code The store-audit script uses an incomplete hostname denylist: ```js function validateSafeUrl(value) { try { const parsed = new URL(value); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { throw new Error(`Invalid protocol: "${parsed.protocol}". Only HTTP and HTTPS are allowed.`); } const hostname = parsed.hostname.toLowerCase(); // Block localhost, loopbacks, and private IP ranges to prevent SSRF const isIp = /^[0-9.]+$/.test(hostname) || hostname.includes(":"); if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname.startsWith("10.") || hostname.startsWith("192.168.") || (hostname.startsWith("172.") && (Number(hostname.split(".")[1]) >= 16 && Number(hostname.split(".")[1]) <= 31))) { throw new Error(`Access to private address "${hostname}" is blocked.`); } return parsed.href; } catch (err) { throw new Error(`Invalid or unsafe URL "${value}": ${err.message}`); } } async function fetchPage(url, opts = {}) { try { validateSafeUrl(url); } catch (e) { return { ok: false, status: 0, url, text: '', error: `Request blocked: ${e.message}` }; } const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), opts.timeout || 15000); try { const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'Mozilla/5.0 (compatible; GMC-Auditor/1.0; +https://selofy.com)', 'Accept': 'text/html,application/xhtml+xml,*/*', 'Accept-Language': 'en-US,en;q=0.9', ...opts.headers, }, redirect: ...[truncated 4877 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Replace hostname string matching with address-based validation: - Resolve DNS names before connecting. - Reject every resolved address that is not globally routable. - Cover IPv4 and IPv6 loopback, private, link-local, unique-local, unspecified, multicast, reserved, benchmarking, and documentation ranges. - Normalize unusual IP representations before validation. 2. Validate redirects manually: - Set `redirect: 'manual'`. - Resolve each `Location` header against the current URL. - Repeat protocol, hostname, DNS, and IP validation before following every redirect. - Enforce a small redirect limit. 3. Restrict crawl scope: - Require sitemap, policy, collection, and product URLs to use the audited store's origin. - If Shopify CDN or alternate storefront domains must be supported, use a narrowly defined allowlist or require explicit user approval. 4. Mitigate DNS rebinding: - Resolve and validate immediately before connection. - Where the HTTP stack permits it, connect to the validated address while preserving the original hostname for TLS and the `Host` header. - Reject any connection whose effective remote address differs from the validated public address. 5. Apply outbound network controls: - Run the scripts in a sandbox that denies access to metadata, loopback, private, and link-local networks. - Permit outbound traffic only to HTTP/HTTPS public storefront destinations required by the audit. 6. Add automated tests for: - `169.254.169.254` - `127.0.0.0/8` - IPv6 `::1`, `fc00::/7`, and `fe80::/10` - Hostnames resolving to private addresses - Public URLs redirecting to prohibited destinations - Sitemap entries referencing off-origin internal addresses ]]>
