{"record":{"id":"8620804bded8460e","repo":"HKUDS/Vibe-Trading","slug":"unsafe-media-url-error","errorCode":null,"errorMessage":"unsafe media URL: {error}","messagePattern":"unsafe media URL: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agent/src/channels/telegram.py","lineNumber":791,"sourceCode":"                    \"video\": self._app.bot.send_video,\n                    \"voice\": self._app.bot.send_voice,\n                    \"audio\": self._app.bot.send_audio,\n                }.get(media_type, self._app.bot.send_document)\n                param = {\n                    \"photo\": \"photo\",\n                    \"video\": \"video\",\n                    \"voice\": \"voice\",\n                    \"audio\": \"audio\",\n                }.get(media_type, \"document\")\n                extra: dict[str, Any] = {}\n                if media_type == \"video\":\n                    extra[\"supports_streaming\"] = True\n\n                # Telegram Bot API accepts HTTP(S) URLs directly for media params.\n                if self._is_remote_media_url(media_path):\n                    ok, error = validate_url_target(media_path)\n                    if not ok:\n                        raise ValueError(f\"unsafe media URL: {error}\")\n                    await self._call_with_retry(\n                        sender,\n                        chat_id=chat_id,\n                        **{param: media_path},\n                        reply_parameters=reply_params,\n                        **thread_kwargs,\n                        **extra,\n                    )\n                    continue\n\n                media_bytes = Path(media_path).read_bytes()\n                filename = Path(media_path).name\n                send_kwargs = {param: media_bytes, \"filename\": filename}\n                await self._call_with_retry(\n                    sender,\n                    chat_id=chat_id,\n                    reply_parameters=reply_params,\n                    **thread_kwargs,","sourceCodeStart":773,"sourceCodeEnd":809,"githubUrl":"https://github.com/HKUDS/Vibe-Trading/blob/80ffdda44c5c4db0dd84d70e051cca591cea67df/agent/src/channels/telegram.py#L773-L809","documentation":"Raised by TelegramChannel.send when media_path is a remote HTTP(S) URL that fails the SSRF safety check validate_url_target. Before passing a URL straight to Telegram's sendPhoto/sendVideo/etc., the channel validates the target to block requests to private/internal networks, so attackers cannot use the bot to probe intranet endpoints.","triggerScenarios":"Calling send() with media_path like \"http://169.254.169.254/latest/meta-data\", \"http://localhost:8080/x.png\", \"https://10.0.0.5/file.jpg\", or a URL whose host resolves to a private IP; validate_url_target returns (False, reason) and the send aborts.","commonSituations":"Media served from an internal service (minio on a LAN IP, k8s service DNS, docker network hostnames); running the agent in a container where a public-looking hostname resolves internally; localhost URLs during local testing; cloud metadata endpoints accidentally used as media sources.","solutions":["Serve media from a genuinely public HTTPS host that resolves to a public IP","For internal media, download the bytes yourself (from inside your trusted network) and pass a local file path or upload bytes instead of a URL","Add an explicit allowlist/egress proxy for trusted internal media hosts and route through it rather than passing raw internal URLs","Check DNS resolution in the runtime environment — a hostname that is public in CI may resolve privately in-cluster"],"exampleFix":"# before\nawait channel.send(\"chat\", text=\"hi\", media_path=\"http://10.0.0.5/media/cat.jpg\")\n# after\nasync with httpx.AsyncClient() as c:  # fetch internally, then send local file\n    r = await c.get(\"http://10.0.0.5/media/cat.jpg\")\n    p = Path(\"/tmp/cat.jpg\"); p.write_bytes(r.content)\nawait channel.send(\"chat\", text=\"hi\", media_path=str(p))","handlingStrategy":"fallback","validationCode":"from agent.src.utils.url_safety import validate_url_target  # same helper the channel uses\n\nasync def safe_media(url: str) -> str | None:\n    ok, _err = validate_url_target(url)\n    return url if ok else None\n\nurl = await safe_media(media_path)\nif url is None:\n    # pre-download and send local file instead\n    ...","typeGuard":"def is_public_media_url(u: str) -> bool:\n    ok, _ = validate_url_target(u)\n    return ok","tryCatchPattern":"try:\n    await channel.send(chat, text=msg, media_path=media_url)\nexcept ValueError as e:\n    if \"unsafe media URL\" in str(e):\n        data = await fetch_bytes_locally(media_url)  # trusted internal fetch\n        await channel.send(chat, text=msg, media_path=await save_tmp(data))\n    else:\n        raise","preventionTips":["Keep media on public HTTPS CDNs when sending by URL","For internal media, fetch bytes inside the network and send a local file","Log the specific validate_url_target error reason to identify private-IP or DNS issues"],"tags":["telegram","ssrf","media","url-validation","security"],"backgroundTag":"ssrf-url-blocked","analyzedSha":"80ffdda44c5c4db0dd84d70e051cca591cea67df","analyzedAt":"2026-08-28T12:46:38.989Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}