crewAIInc/crewAI · error · ValueError

URL '{url}' resolves to private/reserved IP {ip_str}. Access

Error message

URL '{url}' resolves to private/reserved IP {ip_str}. Access to internal networks is not allowed. Set {_UNSAFE_PATHS_ENV}=true to bypass.

What it means

The core SSRF protection in crewai_tools: after resolving a URL's hostname, every returned address is checked against private/reserved ranges (loopback, RFC1918, link-local, etc.). If any resolved IP is private or reserved, the request is refused with this ValueError. This prevents agents from being tricked into hitting internal services (e.g. cloud metadata at 169.254.169.254).

Source

Thrown at lib/crewai-tools/src/crewai_tools/security/safe_path.py:236

    if parsed.scheme not in ("http", "https"):
        raise ValueError(
            f"URL scheme '{parsed.scheme}' is not allowed. Only http and https are supported."
        )

    if not parsed.hostname:
        raise ValueError(f"URL has no hostname: '{url}'")

    try:
        addrinfos = socket.getaddrinfo(
            parsed.hostname, parsed.port or (443 if parsed.scheme == "https" else 80)
        )
    except socket.gaierror as exc:
        raise ValueError(f"Could not resolve hostname: '{parsed.hostname}'") from exc

    for _family, _, _, _, sockaddr in addrinfos:
        ip_str = str(sockaddr[0])
        if _is_private_or_reserved(ip_str):
            raise ValueError(
                f"URL '{url}' resolves to private/reserved IP {ip_str}. "
                f"Access to internal networks is not allowed. "
                f"Set {_UNSAFE_PATHS_ENV}=true to bypass."
            )

    return url

View on GitHub (pinned to 754d7323be)

Solutions

  1. If the target is a legitimate internal/local service you control, set the documented bypass env var (e.g. CREWAI_TOOLS_UNSAFE_PATHS/true per _UNSAFE_PATHS_ENV) only in that trusted environment.
  2. Point the tool at the public address of the service instead of the private one.
  3. If this appears while processing untrusted web content, treat it as the guard working as intended — do not bypass; audit where the URL came from.
  4. For local testing, prefer passing a file path (for file-backed tools) over a localhost URL.

Example fix

# before (guard blocks internal target)
tool.run("http://localhost:8000/report")

# after (explicit opt-in for trusted local testing only)
# export CREWAI_UNSAFE_PATHS=true  (name per _UNSAFE_PATHS_ENV in safe_path.py)
tool.run("http://localhost:8000/report")
Defensive patterns

Strategy: try-catch

Validate before calling

import ipaddress, socket
from urllib.parse import urlparse

def resolves_to_public_only(url: str) -> bool:
    host = urlparse(url).hostname
    if not host:
        return False
    try:
        infos = socket.getaddrinfo(host, None)
    except socket.gaierror:
        return False
    ips = {ipaddress.ip_address(i[4][0]) for i in infos}
    return all(ip.is_global for ip in ips)

Try / catch

try:
    fetch_url_body(url, max_bytes=1_000_000)
except ValueError as e:
    if "private/reserved IP" in str(e):
        # treat as untrusted content; log and skip rather than bypass
        logger.warning("Blocked internal-network URL: %s", url)
        return None
    raise

Prevention

When it happens

Trigger: A URL pointing at 'http://localhost:8080/', 'http://192.168.1.10/api', 'http://169.254.169.254/latest/meta-data/', or a public-looking hostname whose DNS has records that resolve to internal IPs (DNS rebinding or split-horizon DNS).

Common situations: LLM-driven fetch tools following links embedded in scraped content that target internal addresses; developers testing against local dev servers (localhost:8000) through a guarded tool; corporate split-horizon DNS where an internal name resolves differently inside the network.

Related errors


AI-assisted analysis of crewAIInc/crewAI@754d7323be (2026-08-15). Data as JSON: /api/errors/31b8a0413a9d81fb. Report an issue: GitHub.