Skip to content

Fix local MEDIA paths in assistant replies#38572

Merged
obviyus merged 4 commits intomainfrom
fix/reply-media-paths
Mar 7, 2026
Merged

Fix local MEDIA paths in assistant replies#38572
obviyus merged 4 commits intomainfrom
fix/reply-media-paths

Conversation

@obviyus
Copy link
Copy Markdown
Contributor

@obviyus obviyus commented Mar 7, 2026

Summary

  • Fix assistant replies that return local MEDIA: paths as raw relative/container paths.
  • Normalize reply media against the sandbox or agent workspace before block delivery and final payload assembly.
  • Leave MEDIA: parsing and message-tool delivery semantics unchanged.

Change Type

  • Bug fix

Scope

  • Gateway / orchestration
  • Integrations

User-visible / Behavior Changes

  • Normal assistant replies can now send locally generated media instead of falling back to No response generated. Please try again.

Security Impact

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Verification

  • pnpm check
  • Targeted reply-media regressions for workspace-relative paths, sandbox /workspace/... paths, block streaming, and final payload assembly
  • Not verified: live Telegram bot E2E

Risks and Mitigations

  • Risk: a path-like non-file media token gets rewritten.
  • Mitigation: HTTP URLs stay untouched; normalization only runs on reply payload media fields; regressions added.

@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Mar 7, 2026

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Path traversal & arbitrary local file attachment via LLM MEDIA: directives

1. 🟡 Path traversal & arbitrary local file attachment via LLM MEDIA: directives

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 workspaceDir is 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" }) turns text containing MEDIA: into payload.mediaUrl/mediaUrls.
  • splitMediaFromOutput (used by parseReplyDirectives) accepts traversal patterns like ../ (see existing tests: MEDIA:../../etc/passwd).
  • createReplyMediaPathNormalizer then resolves likely-local media using resolvePathFromInput(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 via fileURLToPath and apply the same workspace containment checks.

Analyzed PR: #38572 at commit 2dbc27c

Last updated on: 2026-03-07T06:23:18Z

@openclaw-barnacle openclaw-barnacle bot added size: M maintainer Maintainer-authored PR labels Mar 7, 2026
@obviyus obviyus self-assigned this Mar 7, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR fixes a bug where assistant replies containing local MEDIA: paths (workspace-relative or container-local file:///workspace/... URIs) could be delivered to Telegram as raw unresolved strings, causing media sends to fail and falling back to a "No response generated" message. The fix adds a new createReplyMediaPathNormalizer utility that resolves these paths against the agent's workspaceDir or the sandbox workspace root, and wires it into both the block-streaming delivery path (reply-delivery.ts) and final payload assembly (agent-runner-payloads.ts).

Key changes:

  • New reply-media-paths.ts module encapsulates path-normalization logic: HTTP URLs pass through unchanged, file:///workspace/... container paths are remapped to the host sandbox root via the existing resolveSandboxedMediaSource utility, and bare relative paths are resolved against workspaceDir.
  • buildReplyPayloads is made async to accommodate the async path-normalization hook; all callers updated accordingly.
  • Tests are added at the unit level (reply-media-paths.test.ts), at the wiring level (agent-runner.media-paths.test.ts), and existing block-streaming and payload tests updated to reflect the new normalized path expectations.

Known issue: In reply-media-paths.ts the sandboxRootPromise caches a rejected promise permanently within one normalizer instance — a transient failure from ensureSandboxWorkspaceForSession will cause all subsequent media in that request to fail normalization instead of falling back to workspace-relative resolution. Adding a .catch() that resets the cached promise and returns undefined would make the fallback robust.

Confidence Score: 3/5

  • The core bug fix is correct and well-tested, but a transient sandbox-lookup failure can degrade media delivery for the affected request.
  • The PR's core fix for normalizing media paths is sound and covered by comprehensive tests across unit, integration, and block-streaming layers. However, the sandboxRootPromise caching implementation has a reliability issue: a transient error in ensureSandboxWorkspaceForSession will cause all media normalization for that request to fail instead of gracefully falling back to workspace-relative resolution. This is not a correctness failure under normal operation, but it is a notable edge case that could impact production reliability when transient sandbox-lookup failures occur.
  • src/auto-reply/reply/reply-media-paths.ts — the resolveSandboxRoot closure caches rejected promises; consider adding a .catch() reset before merging if sandbox lookup failures are considered a realistic production scenario.

Last reviewed commit: c254896

Comment on lines +36 to +47
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;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

@obviyus
Copy link
Copy Markdown
Contributor Author

obviyus commented Mar 7, 2026

Follow-up for review:

  • contained async onBlockReply rejections in both fire-and-forget subscribe paths, so malformed streamed media directives no longer surface as unhandled rejections
  • normalized sent-media dedupe keys before final payload filtering, so message.send of ./out/... still dedupes against the assistant reply after reply-media normalization

Verified with targeted regressions plus pnpm check.

@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: L and removed size: M labels Mar 7, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

@obviyus obviyus force-pushed the fix/reply-media-paths branch from e9df3f0 to 2dbc27c Compare March 7, 2026 05:22
@obviyus obviyus merged commit e802840 into main Mar 7, 2026
9 of 10 checks passed
@obviyus obviyus deleted the fix/reply-media-paths branch March 7, 2026 05:22
@obviyus
Copy link
Copy Markdown
Contributor Author

obviyus commented Mar 7, 2026

Landed via temp rebase onto main.

  • Gate: pnpm test -- src/agents/pi-embedded-subscribe.block-reply-rejections.test.ts src/auto-reply/reply/agent-runner-payloads.test.ts src/auto-reply/reply/reply-media-paths.test.ts src/auto-reply/reply/agent-runner.media-paths.test.ts src/auto-reply/reply.block-streaming.test.ts
  • Land commit: 2dbc27c
  • Merge commit: e802840

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +109 to +110
void Promise.resolve()
.then(() => params.onBlockReply?.(payload))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

mrosmarin added a commit to mrosmarin/openclaw that referenced this pull request Mar 7, 2026
* 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)
  ...
mcaxtr pushed a commit to mcaxtr/openclaw that referenced this pull request Mar 7, 2026
vincentkoc pushed a commit to BryanTegomoh/openclaw-fork that referenced this pull request Mar 8, 2026
Saitop pushed a commit to NomiciAI/openclaw that referenced this pull request Mar 8, 2026
jenawant pushed a commit to jenawant/openclaw that referenced this pull request Mar 10, 2026
dhoman pushed a commit to dhoman/chrono-claw that referenced this pull request Mar 11, 2026
senw-developers pushed a commit to senw-developers/va-openclaw that referenced this pull request Mar 17, 2026
V-Gutierrez pushed a commit to V-Gutierrez/openclaw-vendor that referenced this pull request Mar 17, 2026
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 20, 2026
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling maintainer Maintainer-authored PR size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant