langflow-ai/langflow · error · ValueError

webhook url resolves to a blocked address: {', '.join(blocke

Error message

webhook url resolves to a blocked address: {', '.join(blocked)}

What it means

Raised by validate_webhook_url when the webhook host resolves (via resolve_hostname, off-loop) to at least one IP on the SSRF blocklist — private ranges (10/8, 172.16/12, 192.168/16), loopback, link-local (incl. 169.254.169.254 metadata), or other non-routable space. This is a hard floor enforced even when the framework's global SSRF protection is off, because the webhook target is caller-controlled on a public endpoint. The message lists the blocked IPs.

Source

Thrown at src/backend/base/langflow/api/v1/a2a_utils.py:125

    if not parsed.hostname:
        msg = "webhook url has no host"
        raise ValueError(msg)
    if get_settings_service().settings.a2a_allow_private_webhooks:
        return []
    try:
        # Resolve/validate the SAME host httpx connects to (IDNA/punycode raw_host), not the
        # unicode urlparse hostname, so an IDN webhook is pinned/resolved by the exact ASCII host
        # the connection uses (else the pin silently misses: TOCTOU rebind for IDN hosts). httpx.URL
        # raises InvalidURL (not ValueError) for an IDNA-invalid host, so keep it inside the try.
        host = webhook_pin_host(url)
        # Hard floor: reject private/metadata IPs even when global SSRF protection is off
        # (validate_and_resolve_url returns [] with NO enforcement in that case).
        # resolve_hostname handles IP-literal hosts too; the blocking resolve runs off-loop.
        floor_ips = await asyncio.to_thread(resolve_hostname, host)
        blocked = [ip for ip in floor_ips if is_ip_blocked(ip)]
        if blocked:
            msg = f"webhook url resolves to a blocked address: {', '.join(blocked)}"
            raise ValueError(msg)
        # Then the framework check for allowlist / CGNAT / is_global extras + pinned IPs.
        _url, validated_ips = await asyncio.to_thread(validate_and_resolve_url, url)
    except httpx.InvalidURL as exc:
        # Callers only guard ValueError; without translating this an IDNA-invalid host would 500 the
        # caller (or escape dispatch) instead of failing closed as an unsafe webhook.
        msg = f"webhook url has an invalid host: {exc}"
        raise ValueError(msg) from exc
    except SSRFProtectionError as exc:
        msg = f"webhook url is not allowed: {exc}"
        raise ValueError(msg) from exc
    # Fall back to the floor IPs so dispatch can still DNS-pin with the global toggle off.
    return validated_ips or floor_ips


async def folder_auth_type(flow: Flow, session: AsyncSession) -> str:
    """Read the flow's folder ``auth_type`` (``"none"`` | ``"apikey"`` | ``"oauth"``).

    The single source of truth for what the card advertises (resolve_card_security)

View on GitHub (pinned to 976ec789d2)

Solutions

  1. Point the webhook at a publicly routable https URL
  2. For trusted internal networks, set LANGFLOW_A2A_ALLOW_PRIVATE_WEBHOOKS=true (skips the IP floor, returns no pinned IPs)
  3. If the hostname has split-horizon DNS, use a name whose public resolution is global, or front the internal receiver with a public tunnel (e.g. an HTTPS tunnel service)

Example fix

# before
url = "http://192.168.1.5:9000/a2a-hook"
# after
url = "https://a2a-hooks.example.com/hook"  # tunnels to the internal receiver
Defensive patterns

Strategy: validation

Validate before calling

import ipaddress, socket
from urllib.parse import urlparse

def webhook_ips_public(url: str) -> bool:
    host = urlparse(url).hostname or ""
    try:
        ips = {i[4][0] for i in socket.getaddrinfo(host, None)}
    except socket.gaierror:
        return False
    bad = {ip for ip in ips if ipaddress.ip_address(ip).is_private or ipaddress.ip_address(ip).is_loopback or ipaddress.ip_address(ip).is_link_local}
    return not bad

Try / catch

try:
    await client.tasks.set_push_notification(task_id, cfg)
except InvalidParamsError as e:
    if "blocked address" in str(e):
        cfg.url = public_webhook_for(cfg.url)  # tunnel/public endpoint
        await client.tasks.set_push_notification(task_id, cfg)
    else:
        raise

Prevention

When it happens

Trigger: Registering a push config whose URL is http://localhost/…, http://127.0.0.1:8080/…, http://192.168.1.5/hook, a corporate internal hostname resolving to 10.x, or http://169.254.169.254/latest/meta-data (cloud metadata theft attempt).

Common situations: Local development against a deployed server (localhost webhook); pointing at an internal receiver hostname that shares public DNS with an A record to a private IP; genuine SSRF probing of the deployment's internal network.

Related errors


AI-assisted analysis of langflow-ai/langflow@976ec789d2 (2026-08-14). Data as JSON: /api/errors/884e5743ada1a649. Report an issue: GitHub.