mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
fix: reject path traversal and home-dir patterns in media parse layer (#54642)
* fix: reject path traversal and home-dir patterns in media parse layer * Update parse tests
This commit is contained in:
@@ -12,8 +12,6 @@ describe("splitMediaFromOutput", () => {
|
||||
const pathCases = [
|
||||
["/Users/pete/My File.png", "MEDIA:/Users/pete/My File.png"],
|
||||
["/Users/pete/My File.png", 'MEDIA:"/Users/pete/My File.png"'],
|
||||
["~/Pictures/My File.png", "MEDIA:~/Pictures/My File.png"],
|
||||
["../../etc/passwd", "MEDIA:../../etc/passwd"],
|
||||
["./screenshots/image.png", "MEDIA:./screenshots/image.png"],
|
||||
["media/inbound/image.png", "MEDIA:media/inbound/image.png"],
|
||||
["./screenshot.png", " MEDIA:./screenshot.png"],
|
||||
@@ -31,6 +29,21 @@ describe("splitMediaFromOutput", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects traversal and home-dir paths and strips them from output", () => {
|
||||
const traversalCases = [
|
||||
"MEDIA:../../../etc/passwd",
|
||||
"MEDIA:../../.env",
|
||||
"MEDIA:~/.ssh/id_rsa",
|
||||
"MEDIA:~/Pictures/My File.png",
|
||||
"MEDIA:./foo/../../../etc/shadow",
|
||||
];
|
||||
for (const input of traversalCases) {
|
||||
const result = splitMediaFromOutput(input);
|
||||
expect(result.mediaUrls, `should reject media: ${input}`).toBeUndefined();
|
||||
expect(result.text, `should strip from text: ${input}`).toBe("");
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps audio_as_voice detection stable across calls", () => {
|
||||
const input = "Hello [[audio_as_voice]]";
|
||||
const first = splitMediaFromOutput(input);
|
||||
|
||||
@@ -18,10 +18,21 @@ const WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\/]/;
|
||||
const SCHEME_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
||||
const HAS_FILE_EXT = /\.\w{1,10}$/;
|
||||
|
||||
// Recognize local file path patterns. Security validation is deferred to the
|
||||
// load layer (loadWebMedia / resolveSandboxedMediaSource) which has the context
|
||||
// needed to enforce sandbox roots and allowed directories.
|
||||
function isLikelyLocalPath(candidate: string): boolean {
|
||||
// Matches ".." as a standalone path segment (start, middle, or end).
|
||||
const TRAVERSAL_SEGMENT_RE = /(?:^|[/\\])\.\.(?:[/\\]|$)/;
|
||||
|
||||
function hasTraversalOrHomeDirPrefix(candidate: string): boolean {
|
||||
return (
|
||||
candidate.startsWith("../") ||
|
||||
candidate === ".." ||
|
||||
candidate.startsWith("~") ||
|
||||
TRAVERSAL_SEGMENT_RE.test(candidate)
|
||||
);
|
||||
}
|
||||
|
||||
// Broad structural check: does this look like a local file path? Used only for
|
||||
// stripping MEDIA: lines from output text — never for media approval.
|
||||
function looksLikeLocalFilePath(candidate: string): boolean {
|
||||
return (
|
||||
candidate.startsWith("/") ||
|
||||
candidate.startsWith("./") ||
|
||||
@@ -33,6 +44,21 @@ function isLikelyLocalPath(candidate: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
// Recognize safe local file path patterns for media approval, rejecting
|
||||
// traversal and home-dir paths so they never reach downstream load/send logic.
|
||||
function isLikelyLocalPath(candidate: string): boolean {
|
||||
if (hasTraversalOrHomeDirPrefix(candidate)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
candidate.startsWith("/") ||
|
||||
candidate.startsWith("./") ||
|
||||
WINDOWS_DRIVE_RE.test(candidate) ||
|
||||
candidate.startsWith("\\\\") ||
|
||||
(!SCHEME_RE.test(candidate) && (candidate.includes("/") || candidate.includes("\\")))
|
||||
);
|
||||
}
|
||||
|
||||
function isValidMedia(
|
||||
candidate: string,
|
||||
opts?: { allowSpaces?: boolean; allowBareFilename?: boolean },
|
||||
@@ -54,6 +80,12 @@ function isValidMedia(
|
||||
return true;
|
||||
}
|
||||
|
||||
// Hard reject traversal/home-dir patterns before the bare-filename fallback
|
||||
// to prevent path traversal bypasses (e.g. "../../.env" matching HAS_FILE_EXT).
|
||||
if (hasTraversalOrHomeDirPrefix(candidate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Accept bare filenames (e.g. "image.png") only when the caller opts in.
|
||||
// This avoids treating space-split path fragments as separate media items.
|
||||
if (opts?.allowBareFilename && !SCHEME_RE.test(candidate) && HAS_FILE_EXT.test(candidate)) {
|
||||
@@ -169,7 +201,7 @@ export function splitMediaFromOutput(raw: string): {
|
||||
|
||||
const trimmedPayload = payloadValue.trim();
|
||||
const looksLikeLocalPath =
|
||||
isLikelyLocalPath(trimmedPayload) || trimmedPayload.startsWith("file://");
|
||||
looksLikeLocalFilePath(trimmedPayload) || trimmedPayload.startsWith("file://");
|
||||
if (
|
||||
!unwrapped &&
|
||||
validCount === 1 &&
|
||||
|
||||
Reference in New Issue
Block a user