mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 04:40:43 +00:00
fix(qqbot): restrict structured payload local paths (#58453)
* fix(qqbot): restrict structured payload local paths * fix(qqbot): narrow structured payload file access * test(qqbot): cover payload path traversal guards * fix(qqbot): reduce structured payload log exposure * fix(qqbot): preserve inline image payload URLs
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { getHomeDir, resolveQQBotLocalMediaPath } from "./platform.js";
|
||||
import {
|
||||
getHomeDir,
|
||||
resolveQQBotLocalMediaPath,
|
||||
resolveQQBotPayloadLocalFilePath,
|
||||
} from "./platform.js";
|
||||
|
||||
describe("qqbot local media path remapping", () => {
|
||||
const createdPaths: string[] = [];
|
||||
@@ -31,6 +36,7 @@ describe("qqbot local media path remapping", () => {
|
||||
);
|
||||
fs.mkdirSync(path.dirname(mediaFile), { recursive: true });
|
||||
fs.writeFileSync(mediaFile, "image", "utf8");
|
||||
createdPaths.push(path.dirname(mediaFile));
|
||||
|
||||
const missingWorkspacePath = path.join(
|
||||
actualHome,
|
||||
@@ -63,7 +69,110 @@ describe("qqbot local media path remapping", () => {
|
||||
);
|
||||
fs.mkdirSync(path.dirname(mediaFile), { recursive: true });
|
||||
fs.writeFileSync(mediaFile, "image", "utf8");
|
||||
createdPaths.push(path.dirname(mediaFile));
|
||||
|
||||
expect(resolveQQBotLocalMediaPath(mediaFile)).toBe(mediaFile);
|
||||
});
|
||||
|
||||
it("blocks structured payload files outside QQ Bot storage", () => {
|
||||
const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), "qqbot-platform-outside-"));
|
||||
createdPaths.push(outsideRoot);
|
||||
|
||||
const outsideFile = path.join(outsideRoot, "secret.txt");
|
||||
fs.writeFileSync(outsideFile, "secret", "utf8");
|
||||
|
||||
expect(resolveQQBotPayloadLocalFilePath(outsideFile)).toBeNull();
|
||||
});
|
||||
|
||||
it("blocks structured payload paths that escape QQ Bot media via '..'", () => {
|
||||
const escapedPath = path.join(
|
||||
getHomeDir(),
|
||||
".openclaw",
|
||||
"media",
|
||||
"qqbot",
|
||||
"..",
|
||||
"..",
|
||||
"qqbot-escape.txt",
|
||||
);
|
||||
|
||||
expect(resolveQQBotPayloadLocalFilePath(escapedPath)).toBeNull();
|
||||
});
|
||||
|
||||
it("allows structured payload files inside the QQ Bot media directory", () => {
|
||||
const actualHome = getHomeDir();
|
||||
const openclawDir = path.join(actualHome, ".openclaw");
|
||||
fs.mkdirSync(openclawDir, { recursive: true });
|
||||
const testRoot = fs.mkdtempSync(path.join(openclawDir, "qqbot-platform-test-"));
|
||||
createdPaths.push(testRoot);
|
||||
|
||||
const mediaFile = path.join(
|
||||
actualHome,
|
||||
".openclaw",
|
||||
"media",
|
||||
"qqbot",
|
||||
"downloads",
|
||||
path.basename(testRoot),
|
||||
"allowed.png",
|
||||
);
|
||||
fs.mkdirSync(path.dirname(mediaFile), { recursive: true });
|
||||
fs.writeFileSync(mediaFile, "image", "utf8");
|
||||
createdPaths.push(path.dirname(mediaFile));
|
||||
|
||||
expect(resolveQQBotPayloadLocalFilePath(mediaFile)).toBe(mediaFile);
|
||||
});
|
||||
|
||||
it("blocks structured payload files inside the QQ Bot data directory", () => {
|
||||
const actualHome = getHomeDir();
|
||||
const openclawDir = path.join(actualHome, ".openclaw");
|
||||
fs.mkdirSync(openclawDir, { recursive: true });
|
||||
const testRoot = fs.mkdtempSync(path.join(openclawDir, "qqbot-platform-test-"));
|
||||
createdPaths.push(testRoot);
|
||||
|
||||
const dataFile = path.join(
|
||||
actualHome,
|
||||
".openclaw",
|
||||
"qqbot",
|
||||
"sessions",
|
||||
path.basename(testRoot),
|
||||
"session.json",
|
||||
);
|
||||
fs.mkdirSync(path.dirname(dataFile), { recursive: true });
|
||||
fs.writeFileSync(dataFile, "{}", "utf8");
|
||||
createdPaths.push(path.dirname(dataFile));
|
||||
|
||||
expect(resolveQQBotPayloadLocalFilePath(dataFile)).toBeNull();
|
||||
});
|
||||
|
||||
it("allows legacy workspace paths when they remap into QQ Bot media storage", () => {
|
||||
const actualHome = getHomeDir();
|
||||
const openclawDir = path.join(actualHome, ".openclaw");
|
||||
fs.mkdirSync(openclawDir, { recursive: true });
|
||||
const testRoot = fs.mkdtempSync(path.join(openclawDir, "qqbot-platform-test-"));
|
||||
createdPaths.push(testRoot);
|
||||
|
||||
const mediaFile = path.join(
|
||||
actualHome,
|
||||
".openclaw",
|
||||
"media",
|
||||
"qqbot",
|
||||
"downloads",
|
||||
path.basename(testRoot),
|
||||
"legacy.png",
|
||||
);
|
||||
fs.mkdirSync(path.dirname(mediaFile), { recursive: true });
|
||||
fs.writeFileSync(mediaFile, "image", "utf8");
|
||||
createdPaths.push(path.dirname(mediaFile));
|
||||
|
||||
const missingWorkspacePath = path.join(
|
||||
actualHome,
|
||||
".openclaw",
|
||||
"workspace",
|
||||
"qqbot",
|
||||
"downloads",
|
||||
path.basename(testRoot),
|
||||
"legacy.png",
|
||||
);
|
||||
|
||||
expect(resolveQQBotPayloadLocalFilePath(missingWorkspacePath)).toBe(mediaFile);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -154,6 +154,37 @@ export function resolveQQBotLocalMediaPath(p: string): string {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a structured-payload local file path and enforce that it stays within
|
||||
* QQ Bot-owned storage roots.
|
||||
*/
|
||||
export function resolveQQBotPayloadLocalFilePath(p: string): string | null {
|
||||
const candidate = resolveQQBotLocalMediaPath(p);
|
||||
if (!candidate.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedCandidate = path.resolve(candidate);
|
||||
if (!fs.existsSync(resolvedCandidate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const canonicalCandidate = fs.realpathSync(resolvedCandidate);
|
||||
const allowedRoots = [getQQBotMediaDir()];
|
||||
|
||||
for (const root of allowedRoots) {
|
||||
const resolvedRoot = path.resolve(root);
|
||||
const canonicalRoot = fs.existsSync(resolvedRoot)
|
||||
? fs.realpathSync(resolvedRoot)
|
||||
: resolvedRoot;
|
||||
if (isPathWithinRoot(canonicalCandidate, canonicalRoot)) {
|
||||
return canonicalCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Filename normalization.
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user