HKUDS/Vibe-Trading · error · ValueError
unsafe media URL: {error}
Error message
unsafe media URL: {error} What it means
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.
Source
Thrown at agent/src/channels/telegram.py:791
"video": self._app.bot.send_video,
"voice": self._app.bot.send_voice,
"audio": self._app.bot.send_audio,
}.get(media_type, self._app.bot.send_document)
param = {
"photo": "photo",
"video": "video",
"voice": "voice",
"audio": "audio",
}.get(media_type, "document")
extra: dict[str, Any] = {}
if media_type == "video":
extra["supports_streaming"] = True
# Telegram Bot API accepts HTTP(S) URLs directly for media params.
if self._is_remote_media_url(media_path):
ok, error = validate_url_target(media_path)
if not ok:
raise ValueError(f"unsafe media URL: {error}")
await self._call_with_retry(
sender,
chat_id=chat_id,
**{param: media_path},
reply_parameters=reply_params,
**thread_kwargs,
**extra,
)
continue
media_bytes = Path(media_path).read_bytes()
filename = Path(media_path).name
send_kwargs = {param: media_bytes, "filename": filename}
await self._call_with_retry(
sender,
chat_id=chat_id,
reply_parameters=reply_params,
**thread_kwargs,View on GitHub (pinned to 80ffdda44c)
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
Example fix
# before
await channel.send("chat", text="hi", media_path="http://10.0.0.5/media/cat.jpg")
# after
async with httpx.AsyncClient() as c: # fetch internally, then send local file
r = await c.get("http://10.0.0.5/media/cat.jpg")
p = Path("/tmp/cat.jpg"); p.write_bytes(r.content)
await channel.send("chat", text="hi", media_path=str(p)) Defensive patterns
Strategy: fallback
Validate before calling
from agent.src.utils.url_safety import validate_url_target # same helper the channel uses
async def safe_media(url: str) -> str | None:
ok, _err = validate_url_target(url)
return url if ok else None
url = await safe_media(media_path)
if url is None:
# pre-download and send local file instead
... Type guard
def is_public_media_url(u: str) -> bool:
ok, _ = validate_url_target(u)
return ok Try / catch
try:
await channel.send(chat, text=msg, media_path=media_url)
except ValueError as e:
if "unsafe media URL" in str(e):
data = await fetch_bytes_locally(media_url) # trusted internal fetch
await channel.send(chat, text=msg, media_path=await save_tmp(data))
else:
raise Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- webhook_secret_token is required when Telegram mode is webho
- Invalid or missing API key
- API_AUTH_KEY is required for non-local API access
- This file type is not allowed for upload.
- webhook_secret_token must be 1-256 characters using only A-Z
AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28).
Data as JSON: /api/errors/8620804bded8460e.
Report an issue: GitHub.