mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 18:50:41 +00:00
fix(skills): pin validated download roots
This commit is contained in:
@@ -130,22 +130,33 @@ export async function installDownloadSpec(params: {
|
||||
filename = "download";
|
||||
}
|
||||
|
||||
let canonicalSafeRoot = "";
|
||||
let targetDir = "";
|
||||
try {
|
||||
targetDir = resolveDownloadTargetDir(entry, spec);
|
||||
await ensureDir(targetDir);
|
||||
await ensureDir(safeRoot);
|
||||
await assertCanonicalPathWithinBase({
|
||||
baseDir: safeRoot,
|
||||
candidatePath: targetDir,
|
||||
candidatePath: safeRoot,
|
||||
boundaryLabel: "skill tools directory",
|
||||
});
|
||||
canonicalSafeRoot = await fs.promises.realpath(safeRoot);
|
||||
|
||||
const requestedTargetDir = resolveDownloadTargetDir(entry, spec);
|
||||
await ensureDir(requestedTargetDir);
|
||||
await assertCanonicalPathWithinBase({
|
||||
baseDir: safeRoot,
|
||||
candidatePath: requestedTargetDir,
|
||||
boundaryLabel: "skill tools directory",
|
||||
});
|
||||
const targetRelativePath = path.relative(safeRoot, requestedTargetDir);
|
||||
targetDir = path.join(canonicalSafeRoot, targetRelativePath);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { ok: false, message, stdout: "", stderr: message, code: null };
|
||||
}
|
||||
|
||||
const archivePath = path.join(targetDir, filename);
|
||||
const archiveRelativePath = path.relative(safeRoot, archivePath);
|
||||
const archiveRelativePath = path.relative(canonicalSafeRoot, archivePath);
|
||||
if (
|
||||
!archiveRelativePath ||
|
||||
archiveRelativePath === ".." ||
|
||||
@@ -164,7 +175,7 @@ export async function installDownloadSpec(params: {
|
||||
try {
|
||||
const result = await downloadFile({
|
||||
url,
|
||||
rootDir: safeRoot,
|
||||
rootDir: canonicalSafeRoot,
|
||||
relativePath: archiveRelativePath,
|
||||
timeoutMs,
|
||||
});
|
||||
@@ -198,7 +209,7 @@ export async function installDownloadSpec(params: {
|
||||
|
||||
try {
|
||||
await assertCanonicalPathWithinBase({
|
||||
baseDir: safeRoot,
|
||||
baseDir: canonicalSafeRoot,
|
||||
candidatePath: targetDir,
|
||||
boundaryLabel: "skill tools directory",
|
||||
});
|
||||
|
||||
@@ -251,6 +251,47 @@ describe("installDownloadSpec extraction safety", () => {
|
||||
),
|
||||
).toBe("hi");
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"fails closed when the lexical tools root is rebound before the final copy",
|
||||
async () => {
|
||||
const entry = buildEntry("base-rebind");
|
||||
const safeRoot = resolveSkillToolsRootDir(entry);
|
||||
const outsideRoot = path.join(workspaceDir, "outside-root");
|
||||
await fs.mkdir(outsideRoot, { recursive: true });
|
||||
|
||||
fetchWithSsrFGuardMock.mockResolvedValue({
|
||||
response: new Response(
|
||||
new ReadableStream({
|
||||
async start(controller) {
|
||||
controller.enqueue(new Uint8Array(Buffer.from("payload")));
|
||||
const reboundRoot = `${safeRoot}-rebound`;
|
||||
await fs.rename(safeRoot, reboundRoot);
|
||||
await fs.symlink(outsideRoot, safeRoot);
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
release: async () => undefined,
|
||||
});
|
||||
|
||||
const result = await installDownloadSpec({
|
||||
entry,
|
||||
spec: {
|
||||
kind: "download",
|
||||
id: "dl",
|
||||
url: "https://example.invalid/payload.bin",
|
||||
extract: false,
|
||||
targetDir: "runtime",
|
||||
},
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(await fileExists(path.join(outsideRoot, "runtime", "payload.bin"))).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("installDownloadSpec extraction safety (tar.bz2)", () => {
|
||||
|
||||
Reference in New Issue
Block a user