mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-13 19:10:39 +00:00
fix(security): harden file installs and race-path tests
This commit is contained in:
@@ -20,6 +20,7 @@ vi.mock("../process/exec.js", () => ({
|
||||
let installPluginFromArchive: typeof import("./install.js").installPluginFromArchive;
|
||||
let installPluginFromDir: typeof import("./install.js").installPluginFromDir;
|
||||
let installPluginFromNpmSpec: typeof import("./install.js").installPluginFromNpmSpec;
|
||||
let installPluginFromPath: typeof import("./install.js").installPluginFromPath;
|
||||
let runCommandWithTimeout: typeof import("../process/exec.js").runCommandWithTimeout;
|
||||
let suiteTempRoot = "";
|
||||
let tempDirCounter = 0;
|
||||
@@ -308,8 +309,12 @@ afterAll(() => {
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
({ installPluginFromArchive, installPluginFromDir, installPluginFromNpmSpec } =
|
||||
await import("./install.js"));
|
||||
({
|
||||
installPluginFromArchive,
|
||||
installPluginFromDir,
|
||||
installPluginFromNpmSpec,
|
||||
installPluginFromPath,
|
||||
} = await import("./install.js"));
|
||||
({ runCommandWithTimeout } = await import("../process/exec.js"));
|
||||
});
|
||||
|
||||
@@ -598,6 +603,37 @@ describe("installPluginFromDir", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("installPluginFromPath", () => {
|
||||
it("blocks hardlink alias overwrites when installing a plain file plugin", async () => {
|
||||
const baseDir = makeTempDir();
|
||||
const extensionsDir = path.join(baseDir, "extensions");
|
||||
const outsideDir = path.join(baseDir, "outside");
|
||||
fs.mkdirSync(extensionsDir, { recursive: true });
|
||||
fs.mkdirSync(outsideDir, { recursive: true });
|
||||
|
||||
const sourcePath = path.join(baseDir, "payload.js");
|
||||
fs.writeFileSync(sourcePath, "console.log('SAFE');\n", "utf-8");
|
||||
const victimPath = path.join(outsideDir, "victim.js");
|
||||
fs.writeFileSync(victimPath, "ORIGINAL", "utf-8");
|
||||
|
||||
const targetPath = path.join(extensionsDir, "payload.js");
|
||||
fs.linkSync(victimPath, targetPath);
|
||||
|
||||
const result = await installPluginFromPath({
|
||||
path: sourcePath,
|
||||
extensionsDir,
|
||||
mode: "update",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.error.toLowerCase()).toMatch(/hardlink|path alias escape/);
|
||||
expect(fs.readFileSync(victimPath, "utf-8")).toBe("ORIGINAL");
|
||||
});
|
||||
});
|
||||
|
||||
describe("installPluginFromNpmSpec", () => {
|
||||
it("uses --ignore-scripts for npm pack and cleans up temp dir", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { MANIFEST_KEY } from "../compat/legacy-names.js";
|
||||
import { fileExists, readJsonFile, resolveArchiveKind } from "../infra/archive.js";
|
||||
import { writeFileFromPathWithinRoot } from "../infra/fs-safe.js";
|
||||
import { resolveExistingInstallPath, withExtractedArchiveRoot } from "../infra/install-flow.js";
|
||||
import {
|
||||
resolveInstallModeOptions,
|
||||
@@ -401,7 +402,15 @@ export async function installPluginFromFile(params: {
|
||||
}
|
||||
|
||||
logger.info?.(`Installing to ${targetFile}…`);
|
||||
await fs.copyFile(filePath, targetFile);
|
||||
try {
|
||||
await writeFileFromPathWithinRoot({
|
||||
rootDir: extensionsDir,
|
||||
relativePath: path.basename(targetFile),
|
||||
sourcePath: filePath,
|
||||
});
|
||||
} catch (err) {
|
||||
return { ok: false, error: String(err) };
|
||||
}
|
||||
|
||||
return buildFileInstallResult(pluginId, targetFile);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user