mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 07:02:57 +00:00
fix(browser): keep inbound upload paths flat
This commit is contained in:
@@ -324,6 +324,26 @@ describe("resolveExistingUploadPaths", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects nested absolute inbound media paths", async () => {
|
||||
await withFixtureRoot(async ({ inboundMediaDir, uploadsDir }) => {
|
||||
const nestedDir = path.join(inboundMediaDir, "nested");
|
||||
await fs.mkdir(nestedDir, { recursive: true });
|
||||
const nestedFile = path.join(nestedDir, "secret.pdf");
|
||||
await fs.writeFile(nestedFile, "secret", "utf8");
|
||||
|
||||
const result = await resolveExistingUploadPaths({
|
||||
uploadDir: uploadsDir,
|
||||
inboundMediaDir,
|
||||
requestedPaths: [nestedFile],
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toContain("direct child of inbound media directory");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects files outside both managed upload roots", async () => {
|
||||
await withFixtureRoot(async ({ baseDir, inboundMediaDir, uploadsDir }) => {
|
||||
const outsideFile = path.join(baseDir, "secret.txt");
|
||||
@@ -458,6 +478,26 @@ describe("resolveStrictExistingUploadPaths", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects nested absolute inbound media paths at use time", async () => {
|
||||
await withFixtureRoot(async ({ inboundMediaDir, uploadsDir }) => {
|
||||
const nestedDir = path.join(inboundMediaDir, "nested");
|
||||
await fs.mkdir(nestedDir, { recursive: true });
|
||||
const nestedFile = path.join(nestedDir, "secret.pdf");
|
||||
await fs.writeFile(nestedFile, "secret", "utf8");
|
||||
|
||||
const result = await resolveStrictExistingUploadPaths({
|
||||
uploadDir: uploadsDir,
|
||||
inboundMediaDir,
|
||||
requestedPaths: [nestedFile],
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toContain("direct child of inbound media directory");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolvePathWithinRoot", () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
import { CONFIG_DIR } from "../utils.js";
|
||||
@@ -130,6 +131,61 @@ function resolveManagedInboundMediaRefs(params: {
|
||||
return { ok: true, paths };
|
||||
}
|
||||
|
||||
async function isDirectInboundMediaFile(params: {
|
||||
inboundMediaDir: string;
|
||||
resolvedPath: string;
|
||||
}): Promise<boolean> {
|
||||
let inboundRoot: string;
|
||||
try {
|
||||
inboundRoot = await fs.realpath(params.inboundMediaDir);
|
||||
} catch {
|
||||
inboundRoot = path.resolve(params.inboundMediaDir);
|
||||
}
|
||||
const relativePath = path.relative(inboundRoot, params.resolvedPath);
|
||||
return (
|
||||
Boolean(relativePath) &&
|
||||
relativePath !== ".." &&
|
||||
!relativePath.startsWith(`..${path.sep}`) &&
|
||||
!path.isAbsolute(relativePath) &&
|
||||
!relativePath.includes("/") &&
|
||||
!relativePath.includes("\\")
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveDirectInboundMediaPath(params: {
|
||||
inboundMediaDir: string;
|
||||
requestedPath: string;
|
||||
strict: boolean;
|
||||
}): Promise<ExistingPathsResult | StrictExistingPathsResult> {
|
||||
const inboundPathsResult = params.strict
|
||||
? await resolveStrictExistingPathsWithinRoot({
|
||||
rootDir: params.inboundMediaDir,
|
||||
requestedPaths: [params.requestedPath],
|
||||
scopeLabel: `inbound media directory (${params.inboundMediaDir})`,
|
||||
})
|
||||
: await resolveExistingPathsWithinRoot({
|
||||
rootDir: params.inboundMediaDir,
|
||||
requestedPaths: [params.requestedPath],
|
||||
scopeLabel: `inbound media directory (${params.inboundMediaDir})`,
|
||||
});
|
||||
if (!inboundPathsResult.ok) {
|
||||
return inboundPathsResult;
|
||||
}
|
||||
const resolvedPath = inboundPathsResult.paths[0] ?? params.requestedPath;
|
||||
if (
|
||||
!(await isDirectInboundMediaFile({
|
||||
inboundMediaDir: params.inboundMediaDir,
|
||||
resolvedPath,
|
||||
}))
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Invalid media reference: must be a direct child of inbound media directory (${params.inboundMediaDir})`,
|
||||
};
|
||||
}
|
||||
return inboundPathsResult;
|
||||
}
|
||||
|
||||
export async function resolveExistingUploadPaths({
|
||||
requestedPaths,
|
||||
uploadDir = DEFAULT_UPLOAD_DIR,
|
||||
@@ -155,10 +211,10 @@ export async function resolveExistingUploadPaths({
|
||||
continue;
|
||||
}
|
||||
|
||||
const inboundPathsResult = await resolveExistingPathsWithinRoot({
|
||||
rootDir: inboundMediaDir,
|
||||
requestedPaths: [requestedPath],
|
||||
scopeLabel: `inbound media directory (${inboundMediaDir})`,
|
||||
const inboundPathsResult = await resolveDirectInboundMediaPath({
|
||||
inboundMediaDir,
|
||||
requestedPath,
|
||||
strict: false,
|
||||
});
|
||||
if (!inboundPathsResult.ok) {
|
||||
return inboundPathsResult;
|
||||
@@ -193,10 +249,10 @@ export async function resolveStrictExistingUploadPaths({
|
||||
continue;
|
||||
}
|
||||
|
||||
const inboundPathsResult = await resolveStrictExistingPathsWithinRoot({
|
||||
rootDir: inboundMediaDir,
|
||||
requestedPaths: [requestedPath],
|
||||
scopeLabel: `inbound media directory (${inboundMediaDir})`,
|
||||
const inboundPathsResult = await resolveDirectInboundMediaPath({
|
||||
inboundMediaDir,
|
||||
requestedPath,
|
||||
strict: true,
|
||||
});
|
||||
if (!inboundPathsResult.ok) {
|
||||
return inboundPathsResult;
|
||||
|
||||
Reference in New Issue
Block a user