Fix local MEDIA paths in assistant replies#38572
Conversation
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🟡 Path traversal & arbitrary local file attachment via LLM
|
| Property | Value |
|---|---|
| Severity | Medium |
| CWE | CWE-22 |
| Location | src/auto-reply/reply/reply-media-paths.ts:49-72 |
Description
The reply pipeline parses MEDIA: directives out of model-generated text and then normalizes them into filesystem paths. In non-sandbox mode, the new media path normalizer resolves these paths relative to the run workspace without enforcing workspace containment, so ../ traversal (or absolute paths) can escape the workspace.
Impact:
- Untrusted model output can cause the system to attempt to attach/read arbitrary local files (within any permitted
localRoots), leading to information disclosure. - If
workspaceDiris under a shared allowed root (e.g. a shared temp dir),MEDIA:../<other-session>/...can traverse into other sessions’ workspaces.
Why this is reachable:
normalizeReplyPayloadDirectives({ parseMode: "always" })turnstextcontainingMEDIA:intopayload.mediaUrl/mediaUrls.splitMediaFromOutput(used byparseReplyDirectives) accepts traversal patterns like../(see existing tests:MEDIA:../../etc/passwd).createReplyMediaPathNormalizerthen resolves likely-local media usingresolvePathFromInput(media, workspaceDir)with no boundary check.
Vulnerable code:
if (!isLikelyLocalMediaSource(media)) {
return media;
}
if (FILE_URL_RE.test(media)) {
return media;
}
return resolvePathFromInput(media, params.workspaceDir);This allows inputs like MEDIA:../workspace-other/secret.png (or MEDIA:/path/to/secret) to normalize to a host path outside the intended workspace boundary.
Recommendation
Treat MEDIA: directives from LLM output as untrusted and enforce workspace/sandbox containment.
In non-sandbox mode, restrict local media references to paths that remain under workspaceDir (reject absolute paths outside it and reject .. traversal):
import path from "node:path";
import { toRelativeWorkspacePath } from "../../agents/path-policy.js";
// ...
const normalizeLocalWorkspaceMedia = (media: string) => {
// Throws if it escapes workspaceDir.
const rel = toRelativeWorkspacePath(params.workspaceDir, media, { cwd: params.workspaceDir });
// Allow root files if desired by setting allowRoot: true above.
return path.resolve(params.workspaceDir, rel);
};
// In normalizeMediaSource (host mode):
if (isLikelyLocalMediaSource(media)) {
return normalizeLocalWorkspaceMedia(media);
}Additionally:
- Consider disallowing
~, Windows drive paths, and UNC paths in replies unless explicitly required. - Consider allowing only
https?://URLs plus workspace-relative paths for model-originated directives (and require explicit tool output for anything else). - If
file://is supported, convert viafileURLToPathand apply the same workspace containment checks.
Analyzed PR: #38572 at commit 2dbc27c
Last updated on: 2026-03-07T06:23:18Z
Greptile SummaryThis PR fixes a bug where assistant replies containing local Key changes:
Known issue: In Confidence Score: 3/5
Last reviewed commit: c254896 |
| let sandboxRootPromise: Promise<string | undefined> | undefined; | ||
|
|
||
| const resolveSandboxRoot = async (): Promise<string | undefined> => { | ||
| if (!sandboxRootPromise) { | ||
| sandboxRootPromise = ensureSandboxWorkspaceForSession({ | ||
| config: params.cfg, | ||
| sessionKey: params.sessionKey, | ||
| workspaceDir: params.workspaceDir, | ||
| }).then((sandbox) => sandbox?.workspaceDir); | ||
| } | ||
| return await sandboxRootPromise; | ||
| }; |
There was a problem hiding this comment.
If ensureSandboxWorkspaceForSession rejects (e.g. due to a transient I/O or race condition), the rejected promise is stored in sandboxRootPromise. Because the guard is if (!sandboxRootPromise) — which a rejected-but-non-null promise passes — all subsequent calls to resolveSandboxRoot() during the same normalizer instance's lifetime will immediately re-throw the cached rejection.
The practical impact is that a single transient sandbox-lookup failure will cause every subsequent media URL in the same request to fail normalization instead of gracefully falling back to workspace-relative resolution (the non-sandbox path).
To fix, add a .catch() to the promise chain that resets sandboxRootPromise to undefined and returns undefined, so the next invocation can retry or fall through cleanly to the workspace-dir-relative code path.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/reply-media-paths.ts
Line: 36-47
Comment:
If `ensureSandboxWorkspaceForSession` rejects (e.g. due to a transient I/O or race condition), the rejected promise is stored in `sandboxRootPromise`. Because the guard is `if (!sandboxRootPromise)` — which a rejected-but-non-null promise passes — all subsequent calls to `resolveSandboxRoot()` during the same normalizer instance's lifetime will immediately re-throw the cached rejection.
The practical impact is that a single transient sandbox-lookup failure will cause every subsequent media URL in the same request to fail normalization instead of gracefully falling back to workspace-relative resolution (the non-sandbox path).
To fix, add a `.catch()` to the promise chain that resets `sandboxRootPromise` to `undefined` and returns `undefined`, so the next invocation can retry or fall through cleanly to the workspace-dir-relative code path.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c25489603c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Follow-up for review:
Verified with targeted regressions plus |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 17830293e6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
e9df3f0 to
2dbc27c
Compare
|
Landed via temp rebase onto main.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2dbc27cb41
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| void Promise.resolve() | ||
| .then(() => params.onBlockReply?.(payload)) |
There was a problem hiding this comment.
Invoke block replies synchronously before flush boundaries
Wrapping onBlockReply in Promise.resolve().then(...) defers the callback to a later microtask, which breaks the ordering guarantee in handleToolExecutionStart (src/agents/pi-embedded-subscribe.handlers.tools.ts) that flushes buffered block replies before onBlockReplyFlush. In the tool_execution_start path, flushBlockReplyBuffer() can schedule the block callback here, then onBlockReplyFlush runs immediately, so the block is flushed too early (or delivered after tool events), causing message-boundary reordering and delayed/missed forced flush behavior.
Useful? React with 👍 / 👎.
* main: (45 commits) chore: update dependencies except carbon test(memory): make mcporter EINVAL retry test deterministic refactor(extensions): reuse shared helper primitives refactor(core): extract shared dedup helpers fix(ci): re-enable detect-secrets on main docs: reorder 2026.3.7 changelog highlights chore: bump version to 2026.3.7 fix(android): align run command with app id docs: add changelog entry for Android package rename (openclaw#38712) fix(android): rename app package to ai.openclaw.app fix(gateway): stop stale-socket restarts before first event (openclaw#38643) fix(gateway): skip stale-socket restarts for Telegram polling (openclaw#38405) fix(gateway): invalidate bootstrap cache on session rollover (openclaw#38535) docs: update changelog for reply media delivery (openclaw#38572) fix: contain final reply media normalization failures fix: contain block reply media failures fix: normalize reply media paths Fix owner-only auth and overlapping skill env regressions (openclaw#38548) fix(feishu): disable block streaming to prevent silent reply drops (openclaw#38422) fix: suppress ACP NO_REPLY fragments in console output (openclaw#38436) ...
(cherry picked from commit e802840)
(cherry picked from commit e802840)
Summary
MEDIA:paths as raw relative/container paths.MEDIA:parsing and message-tool delivery semantics unchanged.Change Type
Scope
User-visible / Behavior Changes
No response generated. Please try again.Security Impact
Verification
pnpm check/workspace/...paths, block streaming, and final payload assemblyRisks and Mitigations