mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-01 00:21:46 +00:00
fix(security): harden plugin/hook npm installs
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
resolveArchiveKind,
|
||||
resolvePackedRootDir,
|
||||
} from "../infra/archive.js";
|
||||
import { validateRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
||||
import { parseFrontmatter } from "./frontmatter.js";
|
||||
@@ -356,44 +357,48 @@ export async function installHooksFromArchive(params: {
|
||||
}
|
||||
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hook-"));
|
||||
const extractDir = path.join(tmpDir, "extract");
|
||||
await fs.mkdir(extractDir, { recursive: true });
|
||||
|
||||
logger.info?.(`Extracting ${archivePath}…`);
|
||||
try {
|
||||
await extractArchive({ archivePath, destDir: extractDir, timeoutMs, logger });
|
||||
} catch (err) {
|
||||
return { ok: false, error: `failed to extract archive: ${String(err)}` };
|
||||
}
|
||||
const extractDir = path.join(tmpDir, "extract");
|
||||
await fs.mkdir(extractDir, { recursive: true });
|
||||
|
||||
let rootDir = "";
|
||||
try {
|
||||
rootDir = await resolvePackedRootDir(extractDir);
|
||||
} catch (err) {
|
||||
return { ok: false, error: String(err) };
|
||||
}
|
||||
logger.info?.(`Extracting ${archivePath}…`);
|
||||
try {
|
||||
await extractArchive({ archivePath, destDir: extractDir, timeoutMs, logger });
|
||||
} catch (err) {
|
||||
return { ok: false, error: `failed to extract archive: ${String(err)}` };
|
||||
}
|
||||
|
||||
const manifestPath = path.join(rootDir, "package.json");
|
||||
if (await fileExists(manifestPath)) {
|
||||
return await installHookPackageFromDir({
|
||||
packageDir: rootDir,
|
||||
let rootDir = "";
|
||||
try {
|
||||
rootDir = await resolvePackedRootDir(extractDir);
|
||||
} catch (err) {
|
||||
return { ok: false, error: String(err) };
|
||||
}
|
||||
|
||||
const manifestPath = path.join(rootDir, "package.json");
|
||||
if (await fileExists(manifestPath)) {
|
||||
return await installHookPackageFromDir({
|
||||
packageDir: rootDir,
|
||||
hooksDir: params.hooksDir,
|
||||
timeoutMs,
|
||||
logger,
|
||||
mode: params.mode,
|
||||
dryRun: params.dryRun,
|
||||
expectedHookPackId: params.expectedHookPackId,
|
||||
});
|
||||
}
|
||||
|
||||
return await installHookFromDir({
|
||||
hookDir: rootDir,
|
||||
hooksDir: params.hooksDir,
|
||||
timeoutMs,
|
||||
logger,
|
||||
mode: params.mode,
|
||||
dryRun: params.dryRun,
|
||||
expectedHookPackId: params.expectedHookPackId,
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
|
||||
return await installHookFromDir({
|
||||
hookDir: rootDir,
|
||||
hooksDir: params.hooksDir,
|
||||
logger,
|
||||
mode: params.mode,
|
||||
dryRun: params.dryRun,
|
||||
expectedHookPackId: params.expectedHookPackId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function installHooksFromNpmSpec(params: {
|
||||
@@ -411,40 +416,48 @@ export async function installHooksFromNpmSpec(params: {
|
||||
const dryRun = params.dryRun ?? false;
|
||||
const expectedHookPackId = params.expectedHookPackId;
|
||||
const spec = params.spec.trim();
|
||||
if (!spec) {
|
||||
return { ok: false, error: "missing npm spec" };
|
||||
const specError = validateRegistryNpmSpec(spec);
|
||||
if (specError) {
|
||||
return { ok: false, error: specError };
|
||||
}
|
||||
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hook-pack-"));
|
||||
logger.info?.(`Downloading ${spec}…`);
|
||||
const res = await runCommandWithTimeout(["npm", "pack", spec], {
|
||||
timeoutMs: Math.max(timeoutMs, 300_000),
|
||||
cwd: tmpDir,
|
||||
env: { COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" },
|
||||
});
|
||||
if (res.code !== 0) {
|
||||
return { ok: false, error: `npm pack failed: ${res.stderr.trim() || res.stdout.trim()}` };
|
||||
}
|
||||
try {
|
||||
logger.info?.(`Downloading ${spec}…`);
|
||||
const res = await runCommandWithTimeout(["npm", "pack", spec, "--ignore-scripts"], {
|
||||
timeoutMs: Math.max(timeoutMs, 300_000),
|
||||
cwd: tmpDir,
|
||||
env: {
|
||||
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
|
||||
NPM_CONFIG_IGNORE_SCRIPTS: "true",
|
||||
},
|
||||
});
|
||||
if (res.code !== 0) {
|
||||
return { ok: false, error: `npm pack failed: ${res.stderr.trim() || res.stdout.trim()}` };
|
||||
}
|
||||
|
||||
const packed = (res.stdout || "")
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
if (!packed) {
|
||||
return { ok: false, error: "npm pack produced no archive" };
|
||||
}
|
||||
const packed = (res.stdout || "")
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
if (!packed) {
|
||||
return { ok: false, error: "npm pack produced no archive" };
|
||||
}
|
||||
|
||||
const archivePath = path.join(tmpDir, packed);
|
||||
return await installHooksFromArchive({
|
||||
archivePath,
|
||||
hooksDir: params.hooksDir,
|
||||
timeoutMs,
|
||||
logger,
|
||||
mode,
|
||||
dryRun,
|
||||
expectedHookPackId,
|
||||
});
|
||||
const archivePath = path.join(tmpDir, packed);
|
||||
return await installHooksFromArchive({
|
||||
archivePath,
|
||||
hooksDir: params.hooksDir,
|
||||
timeoutMs,
|
||||
logger,
|
||||
mode,
|
||||
dryRun,
|
||||
expectedHookPackId,
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export async function installHooksFromPath(params: {
|
||||
|
||||
Reference in New Issue
Block a user