fix(node-host): fail closed on ruby approval preload flags
This commit is contained in:
@@ -548,6 +548,52 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects ruby require preloads that approval cannot bind completely", () => {
|
||||
withFakeRuntimeBin({
|
||||
binName: "ruby",
|
||||
run: () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ruby-require-"));
|
||||
try {
|
||||
fs.writeFileSync(path.join(tmp, "safe.rb"), 'puts "SAFE"\n');
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["ruby", "-r", "attacker", "./safe.rb"],
|
||||
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 ruby load-path flags that can redirect module resolution after approval", () => {
|
||||
withFakeRuntimeBin({
|
||||
binName: "ruby",
|
||||
run: () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ruby-load-path-"));
|
||||
try {
|
||||
fs.writeFileSync(path.join(tmp, "safe.rb"), 'puts "SAFE"\n');
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["ruby", "-I.", "./safe.rb"],
|
||||
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", () => {
|
||||
withFakeRuntimeBin({
|
||||
binName: "node",
|
||||
|
||||
@@ -134,6 +134,8 @@ const NODE_OPTIONS_WITH_FILE_VALUE = new Set([
|
||||
"--require",
|
||||
]);
|
||||
|
||||
const RUBY_UNSAFE_APPROVAL_FLAGS = new Set(["-I", "-r", "--require"]);
|
||||
|
||||
const POSIX_SHELL_OPTIONS_WITH_VALUE = new Set([
|
||||
"--init-file",
|
||||
"--rcfile",
|
||||
@@ -604,6 +606,33 @@ function resolveDenoRunScriptOperandIndex(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function hasRubyUnsafeApprovalFlag(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 === "-r") {
|
||||
return true;
|
||||
}
|
||||
if (token.startsWith("-I") || token.startsWith("-r")) {
|
||||
return true;
|
||||
}
|
||||
if (RUBY_UNSAFE_APPROVAL_FLAGS.has(token.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isMutableScriptRunner(executable: string): boolean {
|
||||
return GENERIC_MUTABLE_SCRIPT_RUNNERS.has(executable) || isInterpreterLikeSafeBin(executable);
|
||||
}
|
||||
@@ -642,6 +671,9 @@ function resolveMutableFileOperandIndex(argv: string[], cwd: string | undefined)
|
||||
return unwrapped.baseIndex + denoIndex;
|
||||
}
|
||||
}
|
||||
if (executable === "ruby" && hasRubyUnsafeApprovalFlag(unwrapped.argv)) {
|
||||
return null;
|
||||
}
|
||||
if (!isMutableScriptRunner(executable)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user