fix: harden exec PATH handling

This commit is contained in:
Peter Steinberger
2026-02-14 19:50:33 +01:00
parent 53af46ffb8
commit 013e8f6b3b
6 changed files with 169 additions and 51 deletions

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { sanitizeEnv } from "./invoke.js";
describe("node-host sanitizeEnv", () => {
it("ignores PATH overrides", () => {
const prev = process.env.PATH;
process.env.PATH = "/usr/bin";
try {
const env = sanitizeEnv({ PATH: "/tmp/evil:/usr/bin" }) ?? {};
expect(env.PATH).toBe("/usr/bin");
} finally {
if (prev === undefined) {
delete process.env.PATH;
} else {
process.env.PATH = prev;
}
}
});
it("blocks dangerous env keys/prefixes", () => {
const prevPythonPath = process.env.PYTHONPATH;
const prevLdPreload = process.env.LD_PRELOAD;
try {
delete process.env.PYTHONPATH;
delete process.env.LD_PRELOAD;
const env =
sanitizeEnv({
PYTHONPATH: "/tmp/pwn",
LD_PRELOAD: "/tmp/pwn.so",
FOO: "bar",
}) ?? {};
expect(env.FOO).toBe("bar");
expect(env.PYTHONPATH).toBeUndefined();
expect(env.LD_PRELOAD).toBeUndefined();
} finally {
if (prevPythonPath === undefined) {
delete process.env.PYTHONPATH;
} else {
process.env.PYTHONPATH = prevPythonPath;
}
if (prevLdPreload === undefined) {
delete process.env.LD_PRELOAD;
} else {
process.env.LD_PRELOAD = prevLdPreload;
}
}
});
});

View File

@@ -135,33 +135,22 @@ function resolveExecAsk(value?: string): ExecAsk {
return value === "off" || value === "on-miss" || value === "always" ? value : "on-miss";
}
function sanitizeEnv(
export function sanitizeEnv(
overrides?: Record<string, string> | null,
): Record<string, string> | undefined {
if (!overrides) {
return undefined;
}
const merged = { ...process.env } as Record<string, string>;
const basePath = process.env.PATH ?? DEFAULT_NODE_PATH;
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") {
const trimmed = value.trim();
if (!trimmed) {
continue;
}
if (!basePath || trimmed === basePath) {
merged[key] = trimmed;
continue;
}
const suffix = `${path.delimiter}${basePath}`;
if (trimmed.endsWith(suffix)) {
merged[key] = trimmed;
}
continue;
}
if (blockedEnvKeys.has(upper)) {