{"record":{"id":"42e1042dc91832dc","repo":"langflow-ai/langflow","slug":"webhook-url-is-not-allowed-exc","errorCode":null,"errorMessage":"webhook url is not allowed: {exc}","messagePattern":"webhook url is not allowed: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/backend/base/langflow/api/v1/a2a_utils.py","lineNumber":135,"sourceCode":"        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)\n    and what the JSON-RPC route enforces, so the two can't drift. Plaintext read,\n    no decrypt. No folder / missing folder -> ``\"none\"`` (public).\n    \"\"\"\n    if flow.folder_id is None:\n        return \"none\"\n    # Query the folder explicitly; lazy-loading flow.folder would raise in async.\n    folder = (await session.exec(select(Folder).where(Folder.id == flow.folder_id))).first()\n    return (folder.auth_settings or {}).get(\"auth_type\", \"none\") if folder else \"none\"\n\n","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/langflow-ai/langflow/blob/976ec789d2886a86de109c044d089d68e96c9a35/src/backend/base/langflow/api/v1/a2a_utils.py#L117-L153","documentation":"Raised by validate_webhook_url when the framework-level SSRF validation (validate_and_resolve_url) rejects the URL with SSRFProtectionError — e.g. the host is not on the configured allowlist, or it resolves into CGNAT/other space the framework blocks beyond the hard floor. This check supplies the allowlist/global-toggle policy on top of the private-IP floor, and the error chains the original framework message.","triggerScenarios":"SSRF protection enabled with an allowlist that does not include the webhook host; the URL resolves to 100.64.0.0/10 (CGNAT) or another range the framework flags as non-global even though it is not in the basic private floor.","commonSituations":"Hardened deployments that set LANGFLOW_SSRF_ALLOWLIST (or equivalent) so only vetted webhook hosts are permitted; webhooks pointing at shared cloud egress IPs that map into CGNAT ranges.","solutions":["Add the webhook host to the server's SSRF allowlist configuration and retry","Use a webhook host that the framework classifies as globally routable and allowlisted","If policy allows, relax the framework SSRF setting — but keep the private-IP floor in mind (it is independent)"],"exampleFix":"# before: host hooks.vendor.io not allowlisted\n# after (server env): LANGFLOW_SSRF_ALLOWLIST=hooks.vendor.io","handlingStrategy":"fallback","validationCode":"def webhook_host_allowlisted(url: str, allowlist: set[str]) -> bool:\n    from urllib.parse import urlparse\n    return (urlparse(url).hostname or \"\") in allowlist","typeGuard":null,"tryCatchPattern":"try:\n    await client.tasks.set_push_notification(task_id, cfg)\nexcept InvalidParamsError as e:\n    if \"not allowed\" in str(e):\n        cfg.url = ALLOWLISTED_HOOK_URL   # fall back to the vetted receiver\n        await client.tasks.set_push_notification(task_id, cfg)\n    else:\n        raise","preventionTips":["Keep the client's webhook host list in sync with the server's SSRF allowlist","Prefer one canonical allowlisted hooks domain for all A2A callbacks","On 'not allowed', request an allowlist change instead of retrying — it is a policy denial"],"tags":["a2a","ssrf","webhook","allowlist"],"backgroundTag":null,"analyzedSha":"976ec789d2886a86de109c044d089d68e96c9a35","analyzedAt":"2026-08-14T18:23:12.227Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}