Advisory Details
Title: Feishu outbound media fetch allows SSRF to loopback and internal hosts
Description:
The Feishu outbound media implementation in openclaw-cn downloads attacker-controlled remote URLs with an unguarded fetch(mediaUrl) call. Because this happens before Feishu upload/authentication completes and bypasses the repository's shared SSRF-guarded media runtime, a reachable attacker-controlled media value can trigger outbound requests from the gateway host to loopback or internal network targets.
Summary
The bundled Feishu extension fetches attacker-controlled remote media URLs with a raw fetch(mediaUrl) before any Feishu upload or authentication succeeds. In deployments where a lower-trust sender, tool, or automation path can influence outbound Feishu media, this lets that actor trigger server-side requests from the gateway host to loopback or internal network targets.
Details
I verified this against the current source, the manual verification notes, and a fresh rerun of the PoC on June 19, 2026.
The issue is in the Feishu extension's outbound media path. src/infra/outbound/message-action-runner.ts reads the user-supplied media parameter, extensions/feishu/src/outbound.ts passes it into sendMediaFeishu(), and extensions/feishu/src/media.ts treats non-local values as remote URLs and fetches them directly:
} else {
const response = await fetch(mediaUrl);
if (!response.ok) {
The relevant data flow is:
message send --channel feishu --media <attacker URL>
-> src/infra/outbound/message-action-runner.ts
-> extensions/feishu/src/outbound.ts
-> extensions/feishu/src/media.ts
-> fetch(mediaUrl)
This bypasses the shared guarded media runtime in src/media/fetch.ts, which already routes remote downloads through fetchWithSsrFGuard(...). The problem is not that a guard is missing globally; it is that this Feishu extension path does not use the existing guard at all.
I also checked tag history for the vulnerable file. extensions/feishu/src/media.ts is present starting at v0.1.5 and remains vulnerable at the highest affected upstream tag v0.2.1. The older 2026.x tag line does not contain this extension file, so it is not the version line used for the affected range or the occurrence permalinks below.
PoC
Prerequisites
- A checkout of the repository with
dist/entry.js already built.
- Node.js 22.12.0 or later.
- Python 3.
- No live Feishu tenant is required.
- The PoC scripts create an isolated temporary
OPENCLAW_STATE_DIR, enable the bundled Feishu plugin, and intentionally point the Feishu domain to https://127.0.0.1:1 so the SSRF-relevant fetch can be observed before later Feishu auth fails.
Reproduction Steps
- Download the verification script from: verification_test.py
- Download the control script from: control-local-file.py
- From the repository root, run the verification PoC:
python3 verification_test.py
- Confirm that the script reports one canary hit for the attacker-controlled URL passed through the official CLI:
node dist/entry.js message send --channel feishu --target user:ou_demo --media http://127.0.0.1:<port>/canary.png
- Run the control PoC:
python3 control-local-file.py
- Confirm that the control keeps the same CLI path but switches
--media to a local file and produces zero canary hits.
Log of Evidence
Fresh rerun on June 19, 2026:
Verification Mode: Integration-Test
Canary URL: http://127.0.0.1:44997/canary.png
Interface: official CLI `openclaw message send --channel feishu --media <url>`
Processing path: CLI -> message send -> Feishu outbound adapter -> sendMediaFeishu -> fetch(mediaUrl)
Canary hits observed: 1
CLI exit code: 1
[feishu] sendMediaFeishu failed: TypeError: Cannot destructure property 'tenant_access_token' ...
[DEFECT-CONFIRMED-WITH-LIMITATIONS]
Control Mode: Integration-Test baseline
Canary URL (should remain untouched): http://127.0.0.1:50721/control-canary.png
Interface: official CLI `openclaw message send --channel feishu --media <local-path>`
Canary hits observed: 0
CLI exit code: 1
[CONTROL-PASS]
The important point is the differential: both runs fail later at Feishu auth because of the intentionally invalid domain, but only the remote-URL path causes a real outbound request to the localhost canary first.
Impact
This is SSRF in a standard outbound media feature. An actor who can influence Feishu outbound media URLs can use the gateway host as an HTTP client against loopback or internal network targets. That can be used for local service discovery, internal HTTP probing, metadata endpoint access attempts, or reaching internal endpoints with side effects. Based on SECURITY.md, this is not an unauthenticated public-gateway bug by itself; the security impact depends on whether accepted channel senders, tool-enabled agents, or other lower-trust automation paths can drive Feishu outbound media with attacker-controlled URLs.
Affected products
- Ecosystem: npm
- Package name: openclaw-cn
- Affected versions: >= 0.1.5, <= 0.2.1
- Patched versions:
Severity
- Severity: Medium
- Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
Weaknesses
- CWE: CWE-918: Server-Side Request Forgery (SSRF)
Occurrences
| Permalink |
Description |
|
const mediaUrl = readStringParam(params, "media", { trim: false }); |
|
The shared outbound runner reads the untrimmed media parameter that later reaches the Feishu extension. |
|
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => { |
|
// Send text first if provided |
|
if (text?.trim()) { |
|
await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined }); |
|
} |
|
|
|
// Upload and send media if URL provided |
|
if (mediaUrl) { |
|
try { |
|
const result = await sendMediaFeishu({ |
|
cfg, |
|
to, |
|
mediaUrl, |
|
accountId: accountId ?? undefined, |
|
}); |
|
The Feishu outbound adapter forwards mediaUrl directly into sendMediaFeishu(). |
|
} else if (mediaUrl) { |
|
if (isLocalPath(mediaUrl)) { |
|
// Local file path - read directly |
|
const filePath = mediaUrl.startsWith("~") |
|
? mediaUrl.replace("~", process.env.HOME ?? "") |
|
: mediaUrl.replace("file://", ""); |
|
|
|
if (!fs.existsSync(filePath)) { |
|
throw new Error(`Local file not found: ${filePath}`); |
|
} |
|
buffer = fs.readFileSync(filePath); |
|
name = fileName ?? path.basename(filePath); |
|
} else { |
|
// Remote URL - fetch |
|
const response = await fetch(mediaUrl); |
|
if (!response.ok) { |
|
throw new Error(`Failed to fetch media from URL: ${response.status}`); |
|
} |
|
buffer = Buffer.from(await response.arrayBuffer()); |
|
name = fileName ?? (path.basename(new URL(mediaUrl).pathname) || "file"); |
|
The vulnerable branch treats non-local values as remote URLs and performs a raw fetch(mediaUrl) without SSRF protections. |
|
export async function fetchRemoteMedia(options: FetchMediaOptions): Promise<FetchMediaResult> { |
|
const { url, fetchImpl, filePathHint, maxBytes, maxRedirects, ssrfPolicy, lookupFn } = options; |
|
|
|
let res: Response; |
|
let finalUrl = url; |
|
let release: (() => Promise<void>) | null = null; |
|
try { |
|
const result = await fetchWithSsrFGuard({ |
|
url, |
|
fetchImpl, |
|
maxRedirects, |
|
policy: ssrfPolicy, |
|
lookupFn, |
|
}); |
|
res = result.response; |
|
finalUrl = result.finalUrl; |
|
release = result.release; |
|
} catch (err) { |
|
The repository already has a guarded remote-media helper using fetchWithSsrFGuard(...); the vulnerable Feishu extension path bypasses this helper entirely. |
Advisory Details
Title: Feishu outbound media fetch allows SSRF to loopback and internal hosts
Description:
The Feishu outbound media implementation in
openclaw-cndownloads attacker-controlled remote URLs with an unguardedfetch(mediaUrl)call. Because this happens before Feishu upload/authentication completes and bypasses the repository's shared SSRF-guarded media runtime, a reachable attacker-controlledmediavalue can trigger outbound requests from the gateway host to loopback or internal network targets.Summary
The bundled Feishu extension fetches attacker-controlled remote media URLs with a raw
fetch(mediaUrl)before any Feishu upload or authentication succeeds. In deployments where a lower-trust sender, tool, or automation path can influence outbound Feishu media, this lets that actor trigger server-side requests from the gateway host to loopback or internal network targets.Details
I verified this against the current source, the manual verification notes, and a fresh rerun of the PoC on June 19, 2026.
The issue is in the Feishu extension's outbound media path.
src/infra/outbound/message-action-runner.tsreads the user-suppliedmediaparameter,extensions/feishu/src/outbound.tspasses it intosendMediaFeishu(), andextensions/feishu/src/media.tstreats non-local values as remote URLs and fetches them directly:The relevant data flow is:
This bypasses the shared guarded media runtime in
src/media/fetch.ts, which already routes remote downloads throughfetchWithSsrFGuard(...). The problem is not that a guard is missing globally; it is that this Feishu extension path does not use the existing guard at all.I also checked tag history for the vulnerable file.
extensions/feishu/src/media.tsis present starting atv0.1.5and remains vulnerable at the highest affected upstream tagv0.2.1. The older2026.xtag line does not contain this extension file, so it is not the version line used for the affected range or the occurrence permalinks below.PoC
Prerequisites
dist/entry.jsalready built.OPENCLAW_STATE_DIR, enable the bundled Feishu plugin, and intentionally point the Feishu domain tohttps://127.0.0.1:1so the SSRF-relevant fetch can be observed before later Feishu auth fails.Reproduction Steps
python3 verification_test.pynode dist/entry.js message send --channel feishu --target user:ou_demo --media http://127.0.0.1:<port>/canary.pngpython3 control-local-file.py--mediato a local file and produces zero canary hits.Log of Evidence
Fresh rerun on June 19, 2026:
The important point is the differential: both runs fail later at Feishu auth because of the intentionally invalid domain, but only the remote-URL path causes a real outbound request to the localhost canary first.
Impact
This is SSRF in a standard outbound media feature. An actor who can influence Feishu outbound media URLs can use the gateway host as an HTTP client against loopback or internal network targets. That can be used for local service discovery, internal HTTP probing, metadata endpoint access attempts, or reaching internal endpoints with side effects. Based on
SECURITY.md, this is not an unauthenticated public-gateway bug by itself; the security impact depends on whether accepted channel senders, tool-enabled agents, or other lower-trust automation paths can drive Feishu outbound media with attacker-controlled URLs.Affected products
Severity
Weaknesses
Occurrences
openclaw-cn/src/infra/outbound/message-action-runner.ts
Line 818 in 558f272
mediaparameter that later reaches the Feishu extension.openclaw-cn/extensions/feishu/src/outbound.ts
Lines 15 to 29 in 558f272
mediaUrldirectly intosendMediaFeishu().openclaw-cn/extensions/feishu/src/media.ts
Lines 523 to 542 in 558f272
fetch(mediaUrl)without SSRF protections.openclaw-cn/src/media/fetch.ts
Lines 71 to 88 in 558f272
fetchWithSsrFGuard(...); the vulnerable Feishu extension path bypasses this helper entirely.