mirror of https://github.com/openclaw/openclaw.git
fix(node-host): harden perl approval binding
This commit is contained in:
parent
2f03de029c
commit
be8d51c301
|
|
@ -26,6 +26,9 @@ Docs: https://docs.openclaw.ai
|
||||||
- Signal/config validation: add `channels.signal.groups` schema support so per-group `requireMention`, `tools`, and `toolsBySender` overrides no longer get rejected during config validation. (#27199) Thanks @unisone.
|
- Signal/config validation: add `channels.signal.groups` schema support so per-group `requireMention`, `tools`, and `toolsBySender` overrides no longer get rejected during config validation. (#27199) Thanks @unisone.
|
||||||
- Config/discovery: accept `discovery.wideArea.domain` in strict config validation so unicast DNS-SD gateway configs no longer fail with an unrecognized-key error. (#35615) Thanks @ingyukoh.
|
- Config/discovery: accept `discovery.wideArea.domain` in strict config validation so unicast DNS-SD gateway configs no longer fail with an unrecognized-key error. (#35615) Thanks @ingyukoh.
|
||||||
- Security/exec approvals: unwrap more `pnpm` runtime forms during approval binding, including `pnpm --reporter ... exec` and direct `pnpm node` file runs, with matching regression coverage and docs updates.
|
- Security/exec approvals: unwrap more `pnpm` runtime forms during approval binding, including `pnpm --reporter ... exec` and direct `pnpm node` file runs, with matching regression coverage and docs updates.
|
||||||
|
|
||||||
|
- Security/exec approvals: fail closed for Perl `-M` and `-I` approval flows so preload and load-path module resolution stays outside approval-backed runtime execution unless the operator uses a broader explicit trust path.
|
||||||
|
|
||||||
## 2026.3.12
|
## 2026.3.12
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
|
||||||
|
|
@ -625,6 +625,75 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects perl module preloads that approval cannot bind completely", () => {
|
||||||
|
withFakeRuntimeBin({
|
||||||
|
binName: "perl",
|
||||||
|
run: () => {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-perl-module-preload-"));
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(path.join(tmp, "safe.pl"), 'print "SAFE\\n";\n');
|
||||||
|
const prepared = buildSystemRunApprovalPlan({
|
||||||
|
command: ["perl", "-MPreload", "./safe.pl"],
|
||||||
|
cwd: tmp,
|
||||||
|
});
|
||||||
|
expect(prepared).toEqual({
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
"SYSTEM_RUN_DENIED: approval cannot safely bind this interpreter/runtime command",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects perl load-path flags that can redirect module resolution after approval", () => {
|
||||||
|
withFakeRuntimeBin({
|
||||||
|
binName: "perl",
|
||||||
|
run: () => {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-perl-load-path-"));
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(path.join(tmp, "safe.pl"), 'print "SAFE\\n";\n');
|
||||||
|
const prepared = buildSystemRunApprovalPlan({
|
||||||
|
command: ["perl", "-Ilib", "./safe.pl"],
|
||||||
|
cwd: tmp,
|
||||||
|
});
|
||||||
|
expect(prepared).toEqual({
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
"SYSTEM_RUN_DENIED: approval cannot safely bind this interpreter/runtime command",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects perl combined preload and load-path flags", () => {
|
||||||
|
withFakeRuntimeBin({
|
||||||
|
binName: "perl",
|
||||||
|
run: () => {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-perl-preload-load-path-"));
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(path.join(tmp, "safe.pl"), 'print "SAFE\\n";\n');
|
||||||
|
const prepared = buildSystemRunApprovalPlan({
|
||||||
|
command: ["perl", "-Ilib", "-MPreload", "./safe.pl"],
|
||||||
|
cwd: tmp,
|
||||||
|
});
|
||||||
|
expect(prepared).toEqual({
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
"SYSTEM_RUN_DENIED: approval cannot safely bind this interpreter/runtime command",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects shell payloads that hide mutable interpreter scripts", () => {
|
it("rejects shell payloads that hide mutable interpreter scripts", () => {
|
||||||
withFakeRuntimeBin({
|
withFakeRuntimeBin({
|
||||||
binName: "node",
|
binName: "node",
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ const NODE_OPTIONS_WITH_FILE_VALUE = new Set([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const RUBY_UNSAFE_APPROVAL_FLAGS = new Set(["-I", "-r", "--require"]);
|
const RUBY_UNSAFE_APPROVAL_FLAGS = new Set(["-I", "-r", "--require"]);
|
||||||
|
const PERL_UNSAFE_APPROVAL_FLAGS = new Set(["-I", "-M", "-m"]);
|
||||||
|
|
||||||
const POSIX_SHELL_OPTIONS_WITH_VALUE = new Set([
|
const POSIX_SHELL_OPTIONS_WITH_VALUE = new Set([
|
||||||
"--init-file",
|
"--init-file",
|
||||||
|
|
@ -668,6 +669,33 @@ function hasRubyUnsafeApprovalFlag(argv: string[]): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasPerlUnsafeApprovalFlag(argv: string[]): boolean {
|
||||||
|
let afterDoubleDash = false;
|
||||||
|
for (let i = 1; i < argv.length; i += 1) {
|
||||||
|
const token = argv[i]?.trim() ?? "";
|
||||||
|
if (!token) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (afterDoubleDash) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (token === "--") {
|
||||||
|
afterDoubleDash = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "-I" || token === "-M" || token === "-m") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (token.startsWith("-I") || token.startsWith("-M") || token.startsWith("-m")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (PERL_UNSAFE_APPROVAL_FLAGS.has(token)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function isMutableScriptRunner(executable: string): boolean {
|
function isMutableScriptRunner(executable: string): boolean {
|
||||||
return GENERIC_MUTABLE_SCRIPT_RUNNERS.has(executable) || isInterpreterLikeSafeBin(executable);
|
return GENERIC_MUTABLE_SCRIPT_RUNNERS.has(executable) || isInterpreterLikeSafeBin(executable);
|
||||||
}
|
}
|
||||||
|
|
@ -709,6 +737,9 @@ function resolveMutableFileOperandIndex(argv: string[], cwd: string | undefined)
|
||||||
if (executable === "ruby" && hasRubyUnsafeApprovalFlag(unwrapped.argv)) {
|
if (executable === "ruby" && hasRubyUnsafeApprovalFlag(unwrapped.argv)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (executable === "perl" && hasPerlUnsafeApprovalFlag(unwrapped.argv)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!isMutableScriptRunner(executable)) {
|
if (!isMutableScriptRunner(executable)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue