diff --git a/gateway/platforms/wecom_callback.py b/gateway/platforms/wecom_callback.py
index e08bc039742..b656d2ecf96 100644
--- a/gateway/platforms/wecom_callback.py
+++ b/gateway/platforms/wecom_callback.py
@@ -17,7 +17,11 @@ import logging
import socket as _socket
import time
from typing import Any, Dict, List, Optional
-from xml.etree import ElementTree as ET
+# Security: parse untrusted, pre-auth request bodies (WeCom callbacks) with
+# 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:
from aiohttp import web
diff --git a/web/src/components/Markdown.tsx b/web/src/components/Markdown.tsx
index bef0804e7c4..a78c4430c34 100644
--- a/web/src/components/Markdown.tsx
+++ b/web/src/components/Markdown.tsx
@@ -324,11 +324,24 @@ function InlineContent({
);
- case "link":
+ case "link": {
+ // Security: only render http(s)/mailto links. Other schemes
+ // (javascript:, data:, vbscript:) are dropped to plain text so a
+ // crafted link in agent/message content can't execute on click.
+ const href = node.href.trim();
+ if (!/^(https?:|mailto:)/i.test(href)) {
+ return (
+
+ );
+ }
return (
);
+ }
case "br":
return
;
}