diff --git a/gateway/platforms/wecom_callback.py b/gateway/platforms/wecom_callback.py index b656d2ecf96..4335f156f18 100644 --- a/gateway/platforms/wecom_callback.py +++ b/gateway/platforms/wecom_callback.py @@ -21,7 +21,13 @@ from typing import Any, Dict, List, Optional # defusedxml to block billion-laughs / entity-expansion (and XXE) DoS. The # parsing API (fromstring) is a drop-in for the stdlib calls used below; # response-building XML lives in wecom_crypto.py and is not parsed here. -import defusedxml.ElementTree as ET +try: + import defusedxml.ElementTree as ET + + DEFUSEDXML_AVAILABLE = True +except ImportError: + ET = None # type: ignore[assignment] + DEFUSEDXML_AVAILABLE = False try: from aiohttp import web @@ -53,7 +59,7 @@ MESSAGE_DEDUP_TTL_SECONDS = 300 def check_wecom_callback_requirements() -> bool: - return AIOHTTP_AVAILABLE and HTTPX_AVAILABLE + return AIOHTTP_AVAILABLE and HTTPX_AVAILABLE and DEFUSEDXML_AVAILABLE class WecomCallbackAdapter(BasePlatformAdapter): diff --git a/gateway/run.py b/gateway/run.py index 9568eab2ceb..7b5ace07067 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -6294,7 +6294,7 @@ class GatewayRunner: check_wecom_callback_requirements, ) if not check_wecom_callback_requirements(): - logger.warning("WeComCallback: aiohttp/httpx not installed") + logger.warning("WeComCallback: aiohttp/httpx/defusedxml not installed") return None return WecomCallbackAdapter(config) diff --git a/pyproject.toml b/pyproject.toml index ae2472b7a10..05ce2fbe009 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,12 @@ messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", cron = [] # croniter is now a core dependency; this extra kept for back-compat slack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1", "aiohttp==3.13.3"] matrix = ["mautrix[encryption]==0.21.0", "Markdown==3.10.2", "aiosqlite==0.22.1", "asyncpg==0.31.0", "aiohttp-socks==0.11.0"] +# WeCom callback-mode adapter — parses untrusted XML POST bodies from +# WeCom-controlled callback endpoints, so we use defusedxml (drop-in +# replacement for stdlib xml.etree.ElementTree) to block billion-laughs +# and XXE. aiohttp/httpx are already in [messaging]; defusedxml lands +# here to keep the dependency local to wecom_callback's threat model. +wecom = ["defusedxml==0.7.1"] cli = ["simple-term-menu==1.6.6"] tts-premium = ["elevenlabs==1.59.0"] voice = [ diff --git a/tools/lazy_deps.py b/tools/lazy_deps.py index 1a8708ef25c..8f38a3eddc8 100644 --- a/tools/lazy_deps.py +++ b/tools/lazy_deps.py @@ -148,6 +148,10 @@ LAZY_DEPS: dict[str, tuple[str, ...]] = { "lark-oapi==1.5.3", "qrcode==7.4.2", ), + # WeCom callback-mode adapter — parses untrusted XML POST bodies. Pulls + # defusedxml only; aiohttp/httpx are core dependencies of every messaging + # adapter and ship via `platform.discord` / `platform.slack` / etc. + "platform.wecom_callback": ("defusedxml==0.7.1",), # ─── Terminal backends ───────────────────────────────────────────────── "terminal.modal": ("modal==1.3.4",), diff --git a/uv.lock b/uv.lock index 1c0dd1cf17d..0f2e508d7f8 100644 --- a/uv.lock +++ b/uv.lock @@ -1772,6 +1772,9 @@ web = [ { name = "fastapi" }, { name = "uvicorn", extra = ["standard"] }, ] +wecom = [ + { name = "defusedxml" }, +] youtube = [ { name = "youtube-transcript-api" }, ] @@ -1794,6 +1797,7 @@ requires-dist = [ { name = "croniter", specifier = "==6.0.0" }, { name = "daytona", marker = "extra == 'daytona'", specifier = "==0.155.0" }, { name = "debugpy", marker = "extra == 'dev'", specifier = "==1.8.20" }, + { name = "defusedxml", marker = "extra == 'wecom'", specifier = "==0.7.1" }, { name = "dingtalk-stream", marker = "extra == 'dingtalk'", specifier = "==0.24.3" }, { name = "discord-py", extras = ["voice"], marker = "extra == 'messaging'", specifier = "==2.7.1" }, { name = "edge-tts", marker = "extra == 'edge-tts'", specifier = "==7.2.7" }, @@ -1876,7 +1880,7 @@ requires-dist = [ { name = "vercel", marker = "extra == 'vercel'", specifier = "==0.5.7" }, { name = "youtube-transcript-api", marker = "extra == 'youtube'", specifier = "==1.2.4" }, ] -provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "vercel", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "cli", "tts-premium", "voice", "pty", "honcho", "mcp", "homeassistant", "sms", "computer-use", "acp", "bedrock", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"] +provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "vercel", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "wecom", "cli", "tts-premium", "voice", "pty", "honcho", "mcp", "homeassistant", "sms", "computer-use", "acp", "bedrock", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"] [[package]] name = "hf-xet"