{"record":{"id":"be8d51e78b5bc21b","repo":"langflow-ai/langflow","slug":"str-exc-be8d51","errorCode":null,"errorMessage":"str(exc)","messagePattern":"str\\(exc\\)","errorType":"error_code","errorClass":"InvalidParamsError","httpStatus":null,"severity":"error","filePath":"src/backend/base/langflow/api/v1/a2a.py","lineNumber":385,"sourceCode":"    via ``get_info_for_dispatch`` and is unaffected.\n    \"\"\"\n    return f\"{resolve_user_scope(context)}:{context.state.get('flow_id', '')}\"\n\n\nclass _SafePushConfigStore(InMemoryPushNotificationConfigStore):\n    \"\"\"Push-notification config store that SSRF-guards the webhook URL at registration.\n\n    The webhook target is caller-controlled on a public endpoint, so reject one that\n    resolves to a private/loopback/link-local address before storing it (rather than\n    letting the sender POST there). In-memory + per-worker for now; durable cross-worker\n    push configs are a later slice (like the streaming queue manager).\n    \"\"\"\n\n    async def set_info(self, task_id: str, notification_config: pb.TaskPushNotificationConfig, context) -> None:\n        try:\n            await validate_webhook_url(notification_config.url)\n        except ValueError as exc:\n            raise InvalidParamsError(message=str(exc)) from exc\n        await super().set_info(task_id, notification_config, context)\n\n\nclass _SafePushNotificationSender(BasePushNotificationSender):\n    \"\"\"Re-validate + DNS-pin the webhook at dispatch, closing the registration-time rebind gap.\n\n    ``_SafePushConfigStore`` validates at registration, but a host can re-resolve to a\n    private/metadata IP afterwards (DNS rebinding). Re-resolve here and pin the connection\n    to the just-validated IPs (via the shared SSRF transport) so a rebind can't land on an\n    internal address. A webhook that now resolves to a blocked address is dropped (logged),\n    matching the SDK's swallow-and-return-False on a failed send.\n    \"\"\"\n\n    async def _dispatch_notification(self, event, push_info, task_id) -> bool:\n        url = push_info.url\n        try:\n            validated_ips = await validate_webhook_url(url)\n        except ValueError:","sourceCodeStart":367,"sourceCodeEnd":403,"githubUrl":"https://github.com/langflow-ai/langflow/blob/976ec789d2886a86de109c044d089d68e96c9a35/src/backend/base/langflow/api/v1/a2a.py#L367-L403","documentation":"Raised by _SafePushConfigStore.set_info when registering an A2A task push-notification webhook whose URL fails validate_webhook_url. The ValueError (bad scheme, no host, blocked/private IP, IDNA-invalid host, or SSRF allowlist denial) is translated into the JSON-RPC InvalidParamsError so the client gets a spec-level error instead of a 500. This is the SSRF guard on a public, caller-controlled webhook URL, enforced at registration time (and re-checked at dispatch).","triggerScenarios":"A2A message/send or tasks/pushNotificationConfig/set with notification_config.url pointing at e.g. http://localhost:9000/hook, http://169.254.169.254/..., ftp://..., a URL with no hostname, or a host that resolves to a private/CGNAT range when SSRF protection is on.","commonSituations":"Local testing with a loopback webhook against a hardened server; pointing webhooks at internal hostnames; typos like 'http:/host' (missing slash yields no host); DNS entries that resolve to 10.x/172.16.x/192.168.x ranges.","solutions":["Use a public https:// webhook URL that resolves to a globally routable IP","For trusted internal networks, set LANGFLOW_A2A_ALLOW_PRIVATE_WEBHOOKS=true on the server (it skips the IP check)","Fix malformed URLs: include scheme http/https and a real hostname; avoid IP-literal private addresses"],"exampleFix":"# before\nconfig.url = \"http://localhost:9000/a2a-hook\"\n# after\nconfig.url = \"https://hooks.example.com/a2a-hook\"","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\nimport ipaddress, socket\n\ndef webhook_url_safe(url: str) -> bool:\n    p = urlparse(url)\n    if p.scheme not in (\"http\", \"https\") or not p.hostname:\n        return False\n    try:\n        infos = socket.getaddrinfo(p.hostname, None)\n    except socket.gaierror:\n        return False\n    return all(\n        not ipaddress.ip_address(i[4][0]).is_private\n        and not ipaddress.ip_address(i[4][0]).is_loopback\n        and not ipaddress.ip_address(i[4][0]).is_link_local\n        for i in infos\n    )","typeGuard":null,"tryCatchPattern":"try:\n    await client.tasks.set_push_notification(task_id, config)\nexcept InvalidParamsError as e:\n    if \"webhook url\" in str(e):\n        config.url = public_tunnel_url()   # replace internal URL with public https\n        await client.tasks.set_push_notification(task_id, config)\n    else:\n        raise","preventionTips":["Register only public https webhook URLs in shared/production deployments","Keep a client-side pre-check mirroring the server rules: scheme, host, resolved-IP policy","For internal receivers use a tunnel; only flip LANGFLOW_A2A_ALLOW_PRIVATE_WEBHOOKS on trusted networks"],"tags":["a2a","ssrf","webhook","json-rpc"],"backgroundTag":null,"analyzedSha":"976ec789d2886a86de109c044d089d68e96c9a35","analyzedAt":"2026-08-14T18:23:12.227Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}