T09 · Insecure Skill Coding Practices
- Location
scripts/url_fetch.py:158- Finding
Headless browser fallback allows SSRF through unfiltered navigation and subresources
- Content
View full analysis
Vulnerability Details
File Location:
scripts/url_fetch.py:158-170
Vulnerability Type: Browser-mediated server-side request forgery
Risk Level: HighRelated call site:
scripts/url_to_markdown.py:375-382Vulnerable code:
python base = [browser, "--headless=new", "--disable-gpu", f"--virtual-time-budget={virtual_time}", "--dump-dom", url] if pin_rule: base.insert(1, "--host-resolver-rules=%s" % pin_rule) is_root = hasattr(os, "getuid") and os.getuid() == 0 if is_root: attempts = [base + ["--no-sandbox"]] else: attempts = [base, base + ["--no-sandbox"]] last_err = None try: for i, cmd in enumerate(attempts): try: with open(html_path, "w", encoding="utf-8", errors="ignore") as fh: subprocess.run( cmd, stdout=fh, stderr=subprocess.DEVNULL, timeout=90, check=True, )python browser = None if args.no_browser else find_browser() fallback_md = "" if browser: ... html = render_with_browser(args.url, browser, args.virtual_time_budget, allow_internal=args.allow_internal)Technical Analysis
The converter validates and pins the user-supplied top-level URL before launching Chromium. However, the resolver rule only covers the original hostname, and Chromium is launched without request interception or network isolation.
Once Chromium executes an attacker-controlled public page, that page can initiate additional requests through JavaScript, frames, images, stylesheets, or other browser resource mechanisms. Those secondary destinations are not passed through
url_security.py. The page can therefore direct Chromium to loopback, private-network, link-local, or cloud-metadata addresses even though the original public URL passed the SSRF guard.The browser fallback is reachable when the initial direct fetch p ...[truncated 1914 chars]
- Remediation
View remediation
Remediation Suggestions
- Intercept every Chromium request and navigation, not only the initial URL. Validate each destination using the same private, loopback, link-local, reserved, multicast, and CGNAT checks implemented in
url_security.py. - Block non-HTTP and non-HTTPS schemes and re-resolve each hostname immediately before connection.
- Run the browser in an isolated network namespace or container with no route to internal networks, loopback services outside the sandbox, or cloud metadata.
- Alternatively, route browser traffic through a filtering proxy that validates and pins every destination, redirect, frame, and subresource.
- Revalidate the browser's final committed top-level URL before accepting or converting the dumped DOM.
- Disable browser fallback by default until complete request filtering is available, or require explicit user approval after clearly describing the internal-network exposure.
- Add regression tests covering JavaScript navigation, iframe loads, image requests, redirects, IPv4 and IPv6 private addresses, alternate IP representations, and cloud-metadata destinations.
- Intercept every Chromium request and navigation, not only the initial URL. Validate each destination using the same private, loopback, link-local, reserved, multicast, and CGNAT checks implemented in
