Skip to content

media: allow temp-dir MEDIA paths for tool outputs#7400

Closed
grammakov wants to merge 2 commits intoopenclaw:mainfrom
grammakov:fix/media-temp-dir-paths
Closed

media: allow temp-dir MEDIA paths for tool outputs#7400
grammakov wants to merge 2 commits intoopenclaw:mainfrom
grammakov:fix/media-temp-dir-paths

Conversation

@grammakov
Copy link
Copy Markdown

@grammakov grammakov commented Feb 2, 2026

Problem

MEDIA paths from internal tools like tts are rejected by isValidMedia() because they use absolute paths under the OS temp directory (e.g., /tmp/tts-abc123/voice.opus).

This causes TTS voice responses to be sent as plain text markup instead of voice messages, since the media file path fails validation.

Reproduction

  1. Configure TTS with messages.tts.auto: "tagged"
  2. Send a message requesting voice output (e.g., "tell me a joke, voice")
  3. TTS tool generates a file in the temp directory and returns MEDIA:<temp-path>
  4. Media validation rejects the absolute path → voice file never attached
  5. User receives [[tts]] markup as plain text

Note: A local hotfix patching dist/ was applied to verify the fix; this PR targets the source.

Solution

Allow absolute paths that safely resolve under temp directories:

  • Support both os.tmpdir() AND /tmp on POSIX (macOS returns /var/folders/... but tools often use /tmp)
  • Normalize paths with path.resolve()
  • Use path.relative() to safely determine containment
  • Reject temp dir roots themselves (must be a file inside)
  • Preserves existing security: URLs, relative ./ paths, and traversal blocking

Security considerations

  • No new attack surface: Only paths resolving strictly inside temp directories are allowed
  • Traversal-safe: path.resolve() normalizes .. before comparison
  • Cross-platform: Uses path.relative() and path.isAbsolute() for Windows compatibility
  • Prefix attack protection: path.relative() correctly handles /tmp-evil/ vs /tmp

Changes

  • src/media/parse.ts: Add getTempRoots() and isSafeTempPath() helpers, update isValidMedia() to allow temp paths
  • src/media/parse.test.ts: Add OS-portable test cases for temp path acceptance and traversal rejection

Testing

  • pnpm vitest run src/media/parse.test.ts — all 16 tests pass
  • pnpm build — compiles successfully
  • New tests: temp path acceptance (os.tmpdir and /tmp on POSIX)
  • New tests: traversal attack rejection
  • Manual: TTS voice messages now work correctly (verified with local patch)

AI Disclosure

This PR was developed with AI assistance (Claude). The code has been reviewed and tested locally.

Greptile Overview

Greptile Summary

This PR extends media token validation to support absolute MEDIA: file paths that resolve under OS temp directories (via os.tmpdir() and /tmp on POSIX), allowing internal tools like TTS to attach generated audio files instead of leaving [[tts]] markup in the message. It adds helper functions (getTempRoots, isSafeTempPath) and new Vitest coverage for accepted temp paths and rejected traversal/prefix attacks.

Overall, the change fits into the existing splitMediaFromOutput pipeline by relaxing isValidMedia() only for a tightly-scoped class of absolute paths while keeping URL handling and ./ relative path requirements unchanged.

Confidence Score: 4/5

  • This PR looks safe to merge with low risk; it narrowly expands accepted MEDIA paths to temp directories and includes targeted tests.
  • Change is localized to media path validation and uses path.resolve + path.relative containment checks plus tests for traversal and prefix edge cases. Main remaining concern is platform-specific path/case semantics (especially Windows/case-insensitive volumes) that could cause incorrect accept/reject behavior.
  • src/media/parse.ts (temp path containment logic on Windows/case-insensitive filesystems)

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +40 to +42
function isSafeTempPath(candidate: string): boolean {
const resolved = path.resolve(candidate);
const tempRoots = getTempRoots();
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.

[P1] Temp-root comparison can be bypassed on case-insensitive filesystems (Windows/macOS) because path.relative() is case-sensitive.

If os.tmpdir() returns a different casing than the candidate path (or attacker-controlled path casing), path.relative(tempRoot, resolved) can start with .. even when the path is actually within the same directory, causing false negatives. Conversely, if some tempRoot comes in with different casing, a path outside could be misclassified as inside on a case-insensitive volume. Consider normalizing both tempRoot and resolved for platform case-sensitivity (e.g., toLowerCase() on win32/darwin, or use realpath-based normalization if available).
Do you expect this code to run on case-insensitive macOS volumes or Windows, and should temp containment treat paths case-insensitively there?

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/media/parse.ts
Line: 40:42

Comment:
[P1] Temp-root comparison can be bypassed on case-insensitive filesystems (Windows/macOS) because `path.relative()` is case-sensitive.

If `os.tmpdir()` returns a different casing than the candidate path (or attacker-controlled path casing), `path.relative(tempRoot, resolved)` can start with `..` even when the path is actually within the same directory, causing false negatives. Conversely, if some tempRoot comes in with different casing, a path outside could be misclassified as inside on a case-insensitive volume. Consider normalizing both `tempRoot` and `resolved` for platform case-sensitivity (e.g., `toLowerCase()` on `win32`/`darwin`, or use `realpath`-based normalization if available).
 Do you expect this code to run on case-insensitive macOS volumes or Windows, and should temp containment treat paths case-insensitively there?

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +74 to +83
it("accepts paths under /tmp on POSIX", function () {
if (path.sep !== "/") {
// Skip on Windows
return;
}
const testPath = path.join("/tmp", "tts-abc123", "voice.opus");
const result = splitMediaFromOutput(`MEDIA:${testPath}`);
expect(result.mediaUrls).toHaveLength(1);
expect(result.mediaUrls?.[0]).toBe(testPath);
});
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.

[P3] This test uses an early return to skip on Windows; Vitest has it.skipIf(...) / describe.skipIf(...) patterns that make skips more visible in test output.

Not required for correctness, but switching to an explicit skip helper can make CI results clearer when the test is intentionally not executed on some platforms.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/media/parse.test.ts
Line: 74:83

Comment:
[P3] This test uses an early `return` to skip on Windows; Vitest has `it.skipIf(...)` / `describe.skipIf(...)` patterns that make skips more visible in test output.

Not required for correctness, but switching to an explicit skip helper can make CI results clearer when the test is intentionally not executed on some platforms.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

grammakov and others added 2 commits February 4, 2026 16:59
MEDIA paths from internal tools like `tts` are rejected by
`isValidMedia()` because they use absolute paths under the OS
temp directory (e.g., `/tmp/tts-abc123/voice.opus`).

This causes TTS voice responses to be sent as plain text markup
instead of voice messages, since the media file path fails
validation.

Solution:
- Add `getTempRoots()` to identify valid temp directories
  (os.tmpdir() + /tmp on POSIX for macOS compatibility)
- Add `isSafeTempPath()` using path.relative() for traversal-safe
  containment checking
- Update `isValidMedia()` to allow temp paths

Security considerations:
- Only paths resolving strictly inside temp directories are allowed
- Traversal-safe: path.resolve() normalizes ".." before comparison
- Cross-platform: uses path.relative() for Windows compatibility
- Prefix attack protection: path.relative() correctly handles
  /tmp-evil/ vs /tmp

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@grammakov grammakov force-pushed the fix/media-temp-dir-paths branch from 920a027 to 281ef8d Compare February 4, 2026 11:59
ikari-pl added a commit to ikari-pl/freeclaw that referenced this pull request Feb 7, 2026
…#7400)

Security fix openclaw#4930 blocked all absolute paths in isValidMedia() to prevent LFI,
but this also blocked paths generated by internal tools like TTS which write to
OS temp directories (/var/folders/... on macOS, /tmp on Linux).

Allow absolute paths that safely resolve under os.tmpdir() or /tmp, with
path traversal protection and case-insensitive comparison for macOS/Windows.

Co-Authored-By: grammakov <[email protected]>
@mudrii

This comment was marked as spam.

@mudrii

This comment was marked as spam.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Mar 7, 2026
@openclaw-barnacle
Copy link
Copy Markdown

Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.

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

Labels

stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants