mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 15:30:39 +00:00
fix(security): block startup-file env injection across host execution paths
This commit is contained in:
@@ -7,7 +7,7 @@ describe("node-host sanitizeEnv", () => {
|
||||
const prev = process.env.PATH;
|
||||
process.env.PATH = "/usr/bin";
|
||||
try {
|
||||
const env = sanitizeEnv({ PATH: "/tmp/evil:/usr/bin" }) ?? {};
|
||||
const env = sanitizeEnv({ PATH: "/tmp/evil:/usr/bin" });
|
||||
expect(env.PATH).toBe("/usr/bin");
|
||||
} finally {
|
||||
if (prev === undefined) {
|
||||
@@ -21,18 +21,21 @@ describe("node-host sanitizeEnv", () => {
|
||||
it("blocks dangerous env keys/prefixes", () => {
|
||||
const prevPythonPath = process.env.PYTHONPATH;
|
||||
const prevLdPreload = process.env.LD_PRELOAD;
|
||||
const prevBashEnv = process.env.BASH_ENV;
|
||||
try {
|
||||
delete process.env.PYTHONPATH;
|
||||
delete process.env.LD_PRELOAD;
|
||||
const env =
|
||||
sanitizeEnv({
|
||||
PYTHONPATH: "/tmp/pwn",
|
||||
LD_PRELOAD: "/tmp/pwn.so",
|
||||
FOO: "bar",
|
||||
}) ?? {};
|
||||
delete process.env.BASH_ENV;
|
||||
const env = sanitizeEnv({
|
||||
PYTHONPATH: "/tmp/pwn",
|
||||
LD_PRELOAD: "/tmp/pwn.so",
|
||||
BASH_ENV: "/tmp/pwn.sh",
|
||||
FOO: "bar",
|
||||
});
|
||||
expect(env.FOO).toBe("bar");
|
||||
expect(env.PYTHONPATH).toBeUndefined();
|
||||
expect(env.LD_PRELOAD).toBeUndefined();
|
||||
expect(env.BASH_ENV).toBeUndefined();
|
||||
} finally {
|
||||
if (prevPythonPath === undefined) {
|
||||
delete process.env.PYTHONPATH;
|
||||
@@ -44,6 +47,34 @@ describe("node-host sanitizeEnv", () => {
|
||||
} else {
|
||||
process.env.LD_PRELOAD = prevLdPreload;
|
||||
}
|
||||
if (prevBashEnv === undefined) {
|
||||
delete process.env.BASH_ENV;
|
||||
} else {
|
||||
process.env.BASH_ENV = prevBashEnv;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("drops dangerous inherited env keys even without overrides", () => {
|
||||
const prevPath = process.env.PATH;
|
||||
const prevBashEnv = process.env.BASH_ENV;
|
||||
try {
|
||||
process.env.PATH = "/usr/bin:/bin";
|
||||
process.env.BASH_ENV = "/tmp/pwn.sh";
|
||||
const env = sanitizeEnv(undefined);
|
||||
expect(env.PATH).toBe("/usr/bin:/bin");
|
||||
expect(env.BASH_ENV).toBeUndefined();
|
||||
} finally {
|
||||
if (prevPath === undefined) {
|
||||
delete process.env.PATH;
|
||||
} else {
|
||||
process.env.PATH = prevPath;
|
||||
}
|
||||
if (prevBashEnv === undefined) {
|
||||
delete process.env.BASH_ENV;
|
||||
} else {
|
||||
process.env.BASH_ENV = prevBashEnv;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
type ExecHostRunResult,
|
||||
} from "../infra/exec-host.js";
|
||||
import { getTrustedSafeBinDirs } from "../infra/exec-safe-bin-trust.js";
|
||||
import { sanitizeHostExecEnv } from "../infra/host-env-security.js";
|
||||
import { validateSystemRunCommandConsistency } from "../infra/system-run-command.js";
|
||||
import { runBrowserProxyCommand } from "./invoke-browser.js";
|
||||
|
||||
@@ -43,17 +44,6 @@ const execHostEnforced = process.env.OPENCLAW_NODE_EXEC_HOST?.trim().toLowerCase
|
||||
const execHostFallbackAllowed =
|
||||
process.env.OPENCLAW_NODE_EXEC_FALLBACK?.trim().toLowerCase() !== "0";
|
||||
|
||||
const blockedEnvKeys = new Set([
|
||||
"NODE_OPTIONS",
|
||||
"PYTHONHOME",
|
||||
"PYTHONPATH",
|
||||
"PERL5LIB",
|
||||
"PERL5OPT",
|
||||
"RUBYOPT",
|
||||
]);
|
||||
|
||||
const blockedEnvPrefixes = ["DYLD_", "LD_"];
|
||||
|
||||
type SystemRunParams = {
|
||||
command: string[];
|
||||
rawCommand?: string | null;
|
||||
@@ -136,33 +126,8 @@ function resolveExecAsk(value?: string): ExecAsk {
|
||||
return value === "off" || value === "on-miss" || value === "always" ? value : "on-miss";
|
||||
}
|
||||
|
||||
export function sanitizeEnv(
|
||||
overrides?: Record<string, string> | null,
|
||||
): Record<string, string> | undefined {
|
||||
if (!overrides) {
|
||||
return undefined;
|
||||
}
|
||||
const merged = { ...process.env } as Record<string, string>;
|
||||
for (const [rawKey, value] of Object.entries(overrides)) {
|
||||
const key = rawKey.trim();
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
const upper = key.toUpperCase();
|
||||
// PATH is part of the security boundary (command resolution + safe-bin checks). Never allow
|
||||
// request-scoped PATH overrides from agents/gateways.
|
||||
if (upper === "PATH") {
|
||||
continue;
|
||||
}
|
||||
if (blockedEnvKeys.has(upper)) {
|
||||
continue;
|
||||
}
|
||||
if (blockedEnvPrefixes.some((prefix) => upper.startsWith(prefix))) {
|
||||
continue;
|
||||
}
|
||||
merged[key] = value;
|
||||
}
|
||||
return merged;
|
||||
export function sanitizeEnv(overrides?: Record<string, string> | null): Record<string, string> {
|
||||
return sanitizeHostExecEnv({ overrides, blockPathOverrides: true });
|
||||
}
|
||||
|
||||
function truncateOutput(raw: string, maxChars: number): { text: string; truncated: boolean } {
|
||||
|
||||
Reference in New Issue
Block a user