fix(security): harden shell env fallback

This commit is contained in:
Peter Steinberger
2026-02-21 20:01:00 +01:00
parent 817905f3a0
commit 25e89cc863
8 changed files with 129 additions and 13 deletions

View File

@@ -1,10 +1,21 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { isTruthyEnvValue } from "./env.js";
const DEFAULT_TIMEOUT_MS = 15_000;
const DEFAULT_MAX_BUFFER_BYTES = 2 * 1024 * 1024;
const DEFAULT_SHELL = "/bin/sh";
const TRUSTED_SHELL_PREFIXES = [
"/bin/",
"/usr/bin/",
"/usr/local/bin/",
"/opt/homebrew/bin/",
"/run/current-system/sw/bin/",
];
let lastAppliedKeys: string[] = [];
let cachedShellPath: string | null | undefined;
let cachedEtcShells: Set<string> | null | undefined;
function resolveTimeoutMs(timeoutMs: number | undefined): number {
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
@@ -13,9 +24,57 @@ function resolveTimeoutMs(timeoutMs: number | undefined): number {
return Math.max(0, timeoutMs);
}
function readEtcShells(): Set<string> | null {
if (cachedEtcShells !== undefined) {
return cachedEtcShells;
}
try {
const raw = fs.readFileSync("/etc/shells", "utf8");
const entries = raw
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith("#") && path.isAbsolute(line));
cachedEtcShells = new Set(entries);
} catch {
cachedEtcShells = null;
}
return cachedEtcShells;
}
function isTrustedShellPath(shell: string): boolean {
if (!path.isAbsolute(shell)) {
return false;
}
const normalized = path.normalize(shell);
if (normalized !== shell) {
return false;
}
// Primary trust anchor: shell registered in /etc/shells.
const registeredShells = readEtcShells();
if (registeredShells?.has(shell)) {
return true;
}
// Fallback for environments where /etc/shells is incomplete/unavailable.
if (!TRUSTED_SHELL_PREFIXES.some((prefix) => shell.startsWith(prefix))) {
return false;
}
try {
fs.accessSync(shell, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
function resolveShell(env: NodeJS.ProcessEnv): string {
const shell = env.SHELL?.trim();
return shell && shell.length > 0 ? shell : "/bin/sh";
if (shell && isTrustedShellPath(shell)) {
return shell;
}
return DEFAULT_SHELL;
}
function execLoginShellEnvZero(params: {
@@ -171,6 +230,7 @@ export function getShellPathFromLoginShell(opts: {
export function resetShellPathCacheForTests(): void {
cachedShellPath = undefined;
cachedEtcShells = undefined;
}
export function getShellEnvAppliedKeys(): string[] {