test: add env -S allowlist bypass regressions

This commit is contained in:
Peter Steinberger
2026-02-24 02:27:22 +00:00
parent 6634030be3
commit 3f923e8313
4 changed files with 50 additions and 2 deletions

View File

@@ -16,7 +16,7 @@ Docs: https://docs.openclaw.ai
- Security/Config writes: block reserved prototype keys in account-id normalization and route account config resolution through own-key lookups, hardening `/allowlist` and account-scoped config paths against prototype-chain pollution.
- Security/Exec: harden `safeBins` long-option validation by rejecting unknown/ambiguous GNU long-option abbreviations and denying sort filesystem-dependent flags (`--random-source`, `--temporary-directory`, `-T`), closing safe-bin denylist bypasses. Thanks @jiseoung.
- Security/Channels: unify dangerous name-matching policy checks (`dangerouslyAllowNameMatching`) across core and extension channels, share mutable-allowlist detectors between `openclaw doctor` and `openclaw security audit`, and scan all configured accounts (not only the default account) in channel security audit findings.
- Security/Exec approvals: enforce canonical wrapper execution plans across allowlist analysis and runtime execution (node host + gateway host), fail closed on semantic `env` wrapper usage, and reject unknown short safe-bin flags to prevent `env -S/--split-string` interpretation-mismatch bypasses. This ships in the next npm release. Thanks @jiseoung for reporting.
- Security/Exec approvals: enforce canonical wrapper execution plans across allowlist analysis and runtime execution (node host + gateway host), fail closed on semantic `env` wrapper usage, and reject unknown short safe-bin flags to prevent `env -S/--split-string` interpretation-mismatch bypasses. This ships in the next npm release. Thanks @tdjackey for reporting.
- Security/Image tool: enforce `tools.fs.workspaceOnly` for sandboxed `image` path resolution so mounted out-of-workspace paths are blocked before media bytes are loaded/sent to vision providers. This ships in the next npm release. Thanks @tdjackey for reporting.
## 2026.2.23 (Unreleased)

View File

@@ -68,6 +68,9 @@ describe("browser control server", () => {
cdpUrl: state.cdpBaseUrl,
targetId: "abcd1234",
url: "https://example.com",
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
});
const click = await postJson<{ ok: boolean }>(`${base}/act`, {

View File

@@ -254,6 +254,35 @@ describe("exec approvals command resolution", () => {
expect(resolution?.rawExecutable).toBe("/usr/bin/env");
});
it("fails closed for env -S even when env itself is allowlisted", () => {
const dir = makeTempDir();
const binDir = path.join(dir, "bin");
fs.mkdirSync(binDir, { recursive: true });
const envName = process.platform === "win32" ? "env.exe" : "env";
const envPath = path.join(binDir, envName);
fs.writeFileSync(envPath, process.platform === "win32" ? "" : "#!/bin/sh\n");
if (process.platform !== "win32") {
fs.chmodSync(envPath, 0o755);
}
const analysis = analyzeArgvCommand({
argv: [envPath, "-S", 'sh -c "echo pwned"'],
cwd: dir,
env: makePathEnv(binDir),
});
const allowlistEval = evaluateExecAllowlist({
analysis,
allowlist: [{ pattern: envPath }],
safeBins: normalizeSafeBins([]),
cwd: dir,
});
expect(analysis.ok).toBe(true);
expect(analysis.segments[0]?.resolution?.policyBlocked).toBe(true);
expect(allowlistEval.allowlistSatisfied).toBe(false);
expect(allowlistEval.segmentSatisfiedBy).toEqual([null]);
});
it("unwraps env wrapper with shell inner executable", () => {
const resolution = resolveCommandResolutionFromArgv(["/usr/bin/env", "bash", "-lc", "echo hi"]);
expect(resolution?.rawExecutable).toBe("bash");

View File

@@ -150,7 +150,6 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
}),
);
});
it("denies ./sh wrapper spoof in allowlist on-miss mode before execution", async () => {
const marker = path.join(os.tmpdir(), `openclaw-wrapper-spoof-${process.pid}-${Date.now()}`);
const runCommand = vi.fn(async () => {
@@ -213,4 +212,21 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
// no-op
}
});
it("denies env -S shell payloads in allowlist mode", async () => {
const { runCommand, sendInvokeResult } = await runSystemInvoke({
preferMacAppExecHost: false,
security: "allowlist",
command: ["env", "-S", 'sh -c "echo pwned"'],
});
expect(runCommand).not.toHaveBeenCalled();
expect(sendInvokeResult).toHaveBeenCalledWith(
expect.objectContaining({
ok: false,
error: expect.objectContaining({
message: expect.stringContaining("allowlist miss"),
}),
}),
);
});
});