Skip to content

[Security] Sandboxed message tool can read host files outside sandboxRoot via media=file://... during attachment hydration #601

Description

@YLChen-007

Advisory Details

Title: Sandboxed message tool can read host files outside sandboxRoot via media=file://... during attachment hydration

Description:

Summary

A path traversal / local file read flaw in openclaw-cn allows a sandboxed agent that has been explicitly granted the built-in message tool to read host files outside its configured sandboxRoot. The issue occurs because createMessageTool() validates path and filePath inputs but fails to forward sandboxRoot into runMessageAction(), leaving the media=file://... attachment path unsandboxed. As a result, attacker-controlled media input can reach the local file read sink and load arbitrary readable host files into outbound attachment buffers.

Details

The vulnerable flow is in the built-in outbound message/attachment path.

At the tool entrypoint, createMessageTool() invokes runMessageAction() without passing sandboxRoot:

const result = await runMessageAction({
  cfg,
  action,
  params,
  defaultAccountId: accountId ?? undefined,
  gateway,
  toolContext,
  agentId: options?.agentSessionKey
    ? resolveSessionAgentId({ sessionKey: options.agentSessionKey, config: cfg })
    : undefined,
  abortSignal: signal,
});

Because sandboxRoot is omitted here, downstream attachment hydration does not receive the sandbox context. hydrateSendAttachmentParams() then resolves media before any equivalent sandbox enforcement and passes it into loadWebMedia():

const mediaSource = mediaHint ?? fileHint;
if (!params.dryRun && !readStringParam(params.args, "buffer", { trim: false }) && mediaSource) {
  const media = await loadWebMedia(mediaSource, maxBytes);
  params.args.buffer = media.buffer.toString("base64");
}

Finally, loadWebMedia() accepts file:// URLs, converts them to local paths, and reads the file from disk:

if (mediaUrl.startsWith("file://")) {
  mediaUrl = fileURLToPath(mediaUrl);
}
...
const data = await fs.readFile(mediaUrl);

The result is a parameter-shape bypass:

  • path=/host/file is expected to be sandbox-constrained
  • media=file:///host/file reaches the same sink without the sandbox guard

I verified this with the real exported message tool boundary and the real attachment hydration logic. The verification PoC shows that a canary file outside the sandbox root is read into the attachment buffer, while a control run using path= against the same file is rejected with Path escapes sandbox root.

Operational scope note: this is not reachable in the default sandbox tool allowlist, because message is not allowed by default. It becomes reachable in realistic deployments where an operator explicitly enables message for a sandboxed agent and an accepted inbound prompt/input can induce the tool call.

PoC

Prerequisites

  • openclaw-cn checked out locally with dependencies installed
  • Node.js 22+
  • Python 3
  • A runtime context where the built-in message tool is available to a sandboxed agent
  • Access to the provided PoC artifact directory

Reproduction Steps

  1. Download the TypeScript driver from: message_tool_sandbox_capture_driver.ts
  2. Download the verification script from: verification_test.py
  3. Download the control script from: control-path-guard.py
  4. From the repository root, run the verification script:
    python3 llm-enhance/cve-finding/similar/path-traversal/CVE-2026-29611-dataflow-message-tool-sandboxroot-bypass-exp/verification_test.py
  5. Confirm that verification_result.json shows ok: true, captureContainsCanary: true, and the returned attachment buffer contains the canary from host_secret.txt.
  6. Run the control script:
    python3 llm-enhance/cve-finding/similar/path-traversal/CVE-2026-29611-dataflow-message-tool-sandboxroot-bypass-exp/control-path-guard.py
  7. Confirm that control_result.json shows ok: false, bufferBytes: 0, and an error containing Path escapes sandbox root.

Log of Evidence

Verification run:

[DEFECT-CONFIRMED-WITH-LIMITATIONS]
mode=Integration-Test
interface=exported message tool execute() -> runMessageAction() -> hydrateSendAttachmentParams() -> loadWebMedia()
driver_capture_contains_canary=True
independent_capture_contains_canary=True
buffer_bytes=54

Verification artifact highlights:

{
  "ok": true,
  "mode": "vuln",
  "bufferBytes": 54,
  "captureContainsCanary": true,
  "capturePreview": "CVE-2026-29611-CANARY-88971305cd6d40edb40cc1d3b9b70f89"
}

Control run:

[CONTROL-PASS]
mode=Integration-Test
interface=exported message tool execute()
variant=path parameter outside sandbox root

Control artifact highlights:

{
  "ok": false,
  "mode": "control",
  "bufferBytes": 0,
  "executionError": "Path escapes sandbox root (...): .../host_secret.txt"
}

Impact

This is a local file read vulnerability at the agent tool boundary. In deployments that explicitly expose message to a sandboxed agent, an attacker who can influence an accepted tool call can cause the agent to read arbitrary host files readable by the openclaw-cn process and package those contents into outbound attachment buffers. Depending on the configured channel action, this can lead directly to exfiltration of local source files, configuration files, session state, or other sensitive host data.

Affected products

  • Ecosystem: npm
  • Package name: openclaw-cn
  • Affected versions: <= 2026.2.5
  • Patched versions:

Severity

  • Severity: High
  • Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Weaknesses

  • CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

Occurrences

Permalink Description
https://github.com/jiulingyun/openclaw-cn/blob/253435917c161937021a14f82462a60b55056984/src/agents/tools/message-tool.ts#L381-L392 createMessageTool() calls runMessageAction() without forwarding sandboxRoot, so downstream attachment handling loses sandbox context.
https://github.com/jiulingyun/openclaw-cn/blob/253435917c161937021a14f82462a60b55056984/src/infra/outbound/message-action-runner.ts#L357-L410 hydrateSendAttachmentParams() prioritizes media and passes the resolved local source directly into loadWebMedia() for buffer hydration.
https://github.com/jiulingyun/openclaw-cn/blob/253435917c161937021a14f82462a60b55056984/src/infra/outbound/message-action-runner.ts#L917-L924 The runner invokes attachment hydration for sendAttachment without any preceding sandbox-media normalization in this release.
https://github.com/jiulingyun/openclaw-cn/blob/253435917c161937021a14f82462a60b55056984/src/web/media.ts#L143-L149 loadWebMedia() accepts file:// URLs and converts them into local filesystem paths.
https://github.com/jiulingyun/openclaw-cn/blob/253435917c161937021a14f82462a60b55056984/src/web/media.ts#L231-L236 The converted local path is read from disk with fs.readFile(), making host file contents available to outbound attachment processing.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions