mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-27 21:33:26 +02:00
fix(lobster): prevent arbitrary exec via lobsterPath/cwd
This commit is contained in:
@@ -114,6 +114,28 @@ describe("lobster plugin tool", () => {
|
||||
).rejects.toThrow(/absolute path/);
|
||||
});
|
||||
|
||||
it("rejects lobsterPath that is not the lobster executable", async () => {
|
||||
const tool = createLobsterTool(fakeApi());
|
||||
await expect(
|
||||
tool.execute("call2b", {
|
||||
action: "run",
|
||||
pipeline: "noop",
|
||||
lobsterPath: "/bin/bash",
|
||||
}),
|
||||
).rejects.toThrow(/lobster executable/);
|
||||
});
|
||||
|
||||
it("rejects absolute cwd", async () => {
|
||||
const tool = createLobsterTool(fakeApi());
|
||||
await expect(
|
||||
tool.execute("call2c", {
|
||||
action: "run",
|
||||
pipeline: "noop",
|
||||
cwd: "/tmp",
|
||||
}),
|
||||
).rejects.toThrow(/cwd must be a relative path/);
|
||||
});
|
||||
|
||||
it("rejects invalid JSON from lobster", async () => {
|
||||
const { binPath } = await writeFakeLobsterScript(
|
||||
`process.stdout.write("nope");\n`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import type { OpenClawPluginApi } from "../../../src/plugins/types.js";
|
||||
@@ -23,12 +24,56 @@ type LobsterEnvelope =
|
||||
|
||||
function resolveExecutablePath(lobsterPathRaw: string | undefined) {
|
||||
const lobsterPath = lobsterPathRaw?.trim() || "lobster";
|
||||
if (lobsterPath !== "lobster" && !path.isAbsolute(lobsterPath)) {
|
||||
throw new Error("lobsterPath must be an absolute path (or omit to use PATH)");
|
||||
|
||||
// SECURITY:
|
||||
// Never allow arbitrary executables (e.g. /bin/bash). If the caller overrides
|
||||
// the path, it must still be the lobster binary (by name) and be absolute.
|
||||
if (lobsterPath !== "lobster") {
|
||||
if (!path.isAbsolute(lobsterPath)) {
|
||||
throw new Error("lobsterPath must be an absolute path (or omit to use PATH)");
|
||||
}
|
||||
const base = path.basename(lobsterPath).toLowerCase();
|
||||
const allowed =
|
||||
process.platform === "win32"
|
||||
? ["lobster.exe", "lobster.cmd", "lobster.bat"]
|
||||
: ["lobster"];
|
||||
if (!allowed.includes(base)) {
|
||||
throw new Error("lobsterPath must point to the lobster executable");
|
||||
}
|
||||
try {
|
||||
const stat = fs.statSync(lobsterPath);
|
||||
if (!stat.isFile()) {
|
||||
throw new Error("lobsterPath must point to a file");
|
||||
}
|
||||
if (process.platform !== "win32") {
|
||||
fs.accessSync(lobsterPath, fs.constants.X_OK);
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`lobsterPath is not executable: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lobsterPath;
|
||||
}
|
||||
|
||||
function resolveCwd(cwdRaw: unknown): string {
|
||||
if (typeof cwdRaw !== "string" || !cwdRaw.trim()) {
|
||||
return process.cwd();
|
||||
}
|
||||
const cwd = cwdRaw.trim();
|
||||
if (path.isAbsolute(cwd)) {
|
||||
throw new Error("cwd must be a relative path");
|
||||
}
|
||||
const base = process.cwd();
|
||||
const resolved = path.resolve(base, cwd);
|
||||
const rel = path.relative(base, resolved);
|
||||
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
||||
throw new Error("cwd must stay within the gateway working directory");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function isWindowsSpawnEINVAL(err: unknown) {
|
||||
if (!err || typeof err !== "object") {
|
||||
return false;
|
||||
@@ -182,8 +227,13 @@ export function createLobsterTool(api: OpenClawPluginApi) {
|
||||
argsJson: Type.Optional(Type.String()),
|
||||
token: Type.Optional(Type.String()),
|
||||
approve: Type.Optional(Type.Boolean()),
|
||||
lobsterPath: Type.Optional(Type.String()),
|
||||
cwd: Type.Optional(Type.String()),
|
||||
lobsterPath: Type.Optional(Type.String({ description: "Absolute path to the lobster executable (optional)." })),
|
||||
cwd: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Relative working directory (optional). Must stay within the gateway working directory.",
|
||||
}),
|
||||
),
|
||||
timeoutMs: Type.Optional(Type.Number()),
|
||||
maxStdoutBytes: Type.Optional(Type.Number()),
|
||||
}),
|
||||
@@ -196,8 +246,7 @@ export function createLobsterTool(api: OpenClawPluginApi) {
|
||||
const execPath = resolveExecutablePath(
|
||||
typeof params.lobsterPath === "string" ? params.lobsterPath : undefined,
|
||||
);
|
||||
const cwd =
|
||||
typeof params.cwd === "string" && params.cwd.trim() ? params.cwd.trim() : process.cwd();
|
||||
const cwd = resolveCwd(params.cwd);
|
||||
const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : 20_000;
|
||||
const maxStdoutBytes =
|
||||
typeof params.maxStdoutBytes === "number" ? params.maxStdoutBytes : 512_000;
|
||||
|
||||
Reference in New Issue
Block a user