{"record":{"id":"884e5743ada1a649","repo":"langflow-ai/langflow","slug":"webhook-url-resolves-to-a-blocked-address-j","errorCode":null,"errorMessage":"webhook url resolves to a blocked address: {', '.join(blocked)}","messagePattern":"webhook url resolves to a blocked address: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/backend/base/langflow/api/v1/a2a_utils.py","lineNumber":125,"sourceCode":"    if not parsed.hostname:\n        msg = \"webhook url has no host\"\n        raise ValueError(msg)\n    if get_settings_service().settings.a2a_allow_private_webhooks:\n        return []\n    try:\n        # Resolve/validate the SAME host httpx connects to (IDNA/punycode raw_host), not the\n        # unicode urlparse hostname, so an IDN webhook is pinned/resolved by the exact ASCII host\n        # the connection uses (else the pin silently misses: TOCTOU rebind for IDN hosts). httpx.URL\n        # raises InvalidURL (not ValueError) for an IDNA-invalid host, so keep it inside the try.\n        host = webhook_pin_host(url)\n        # Hard floor: reject private/metadata IPs even when global SSRF protection is off\n        # (validate_and_resolve_url returns [] with NO enforcement in that case).\n        # resolve_hostname handles IP-literal hosts too; the blocking resolve runs off-loop.\n        floor_ips = await asyncio.to_thread(resolve_hostname, host)\n        blocked = [ip for ip in floor_ips if is_ip_blocked(ip)]\n        if blocked:\n            msg = f\"webhook url resolves to a blocked address: {', '.join(blocked)}\"\n            raise ValueError(msg)\n        # Then the framework check for allowlist / CGNAT / is_global extras + pinned IPs.\n        _url, validated_ips = await asyncio.to_thread(validate_and_resolve_url, url)\n    except httpx.InvalidURL as exc:\n        # Callers only guard ValueError; without translating this an IDNA-invalid host would 500 the\n        # caller (or escape dispatch) instead of failing closed as an unsafe webhook.\n        msg = f\"webhook url has an invalid host: {exc}\"\n        raise ValueError(msg) from exc\n    except SSRFProtectionError as exc:\n        msg = f\"webhook url is not allowed: {exc}\"\n        raise ValueError(msg) from exc\n    # Fall back to the floor IPs so dispatch can still DNS-pin with the global toggle off.\n    return validated_ips or floor_ips\n\n\nasync def folder_auth_type(flow: Flow, session: AsyncSession) -> str:\n    \"\"\"Read the flow's folder ``auth_type`` (``\"none\"`` | ``\"apikey\"`` | ``\"oauth\"``).\n\n    The single source of truth for what the card advertises (resolve_card_security)","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/langflow-ai/langflow/blob/976ec789d2886a86de109c044d089d68e96c9a35/src/backend/base/langflow/api/v1/a2a_utils.py#L107-L143","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Point the webhook at a publicly routable https URL","For trusted internal networks, set LANGFLOW_A2A_ALLOW_PRIVATE_WEBHOOKS=true (skips the IP floor, returns no pinned IPs)","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)"],"exampleFix":"# before\nurl = \"http://192.168.1.5:9000/a2a-hook\"\n# after\nurl = \"https://a2a-hooks.example.com/hook\"  # tunnels to the internal receiver","handlingStrategy":"validation","validationCode":"import ipaddress, socket\nfrom urllib.parse import urlparse\n\ndef webhook_ips_public(url: str) -> bool:\n    host = urlparse(url).hostname or \"\"\n    try:\n        ips = {i[4][0] for i in socket.getaddrinfo(host, None)}\n    except socket.gaierror:\n        return False\n    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}\n    return not bad","typeGuard":null,"tryCatchPattern":"try:\n    await client.tasks.set_push_notification(task_id, cfg)\nexcept InvalidParamsError as e:\n    if \"blocked address\" in str(e):\n        cfg.url = public_webhook_for(cfg.url)  # tunnel/public endpoint\n        await client.tasks.set_push_notification(task_id, cfg)\n    else:\n        raise","preventionTips":["Default to public https webhook endpoints; treat private targets as a deployment smell","For internal receivers use an outbound tunnel rather than weakening the SSRF floor","Watch for split-horizon DNS: validate the resolution the server will see, not your laptop's"],"tags":["a2a","ssrf","webhook","security"],"backgroundTag":null,"analyzedSha":"976ec789d2886a86de109c044d089d68e96c9a35","analyzedAt":"2026-08-14T18:23:12.227Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}