refactor(security): unify safe-bin argv parsing and harden regressions

This commit is contained in:
Peter Steinberger
2026-02-19 16:04:51 +01:00
parent 2e421f32df
commit a688ccf24a
7 changed files with 292 additions and 67 deletions

View File

@@ -13,6 +13,7 @@ import type { ExecAllowlistEntry } from "./exec-approvals.js";
import {
SAFE_BIN_GENERIC_PROFILE,
SAFE_BIN_PROFILES,
type SafeBinProfile,
validateSafeBinArgv,
} from "./exec-safe-bin-policy.js";
import { isTrustedSafeBinPath } from "./exec-safe-bin-trust.js";
@@ -38,11 +39,15 @@ export function isSafeBinUsage(params: {
argv: string[];
resolution: CommandResolution | null;
safeBins: Set<string>;
platform?: string | null;
trustedSafeBinDirs?: ReadonlySet<string>;
safeBinProfiles?: Readonly<Record<string, SafeBinProfile>>;
safeBinGenericProfile?: SafeBinProfile;
isTrustedSafeBinPathFn?: typeof isTrustedSafeBinPath;
}): boolean {
// Windows host exec uses PowerShell, which has different parsing/expansion rules.
// Keep safeBins conservative there (require explicit allowlist entries).
if (isWindowsPlatform(process.platform)) {
if (isWindowsPlatform(params.platform ?? process.platform)) {
return false;
}
if (params.safeBins.size === 0) {
@@ -60,8 +65,9 @@ export function isSafeBinUsage(params: {
if (!resolution?.resolvedPath) {
return false;
}
const isTrustedPath = params.isTrustedSafeBinPathFn ?? isTrustedSafeBinPath;
if (
!isTrustedSafeBinPath({
!isTrustedPath({
resolvedPath: resolution.resolvedPath,
trustedDirs: params.trustedSafeBinDirs,
})
@@ -69,7 +75,9 @@ export function isSafeBinUsage(params: {
return false;
}
const argv = params.argv.slice(1);
const profile = SAFE_BIN_PROFILES[execName] ?? SAFE_BIN_GENERIC_PROFILE;
const safeBinProfiles = params.safeBinProfiles ?? SAFE_BIN_PROFILES;
const genericSafeBinProfile = params.safeBinGenericProfile ?? SAFE_BIN_GENERIC_PROFILE;
const profile = safeBinProfiles[execName] ?? genericSafeBinProfile;
return validateSafeBinArgv(argv, profile);
}
@@ -87,6 +95,7 @@ function evaluateSegments(
allowlist: ExecAllowlistEntry[];
safeBins: Set<string>;
cwd?: string;
platform?: string | null;
trustedSafeBinDirs?: ReadonlySet<string>;
skillBins?: Set<string>;
autoAllowSkills?: boolean;
@@ -114,6 +123,7 @@ function evaluateSegments(
argv: segment.argv,
resolution: segment.resolution,
safeBins: params.safeBins,
platform: params.platform,
trustedSafeBinDirs: params.trustedSafeBinDirs,
});
const skillAllow =
@@ -139,6 +149,7 @@ export function evaluateExecAllowlist(params: {
allowlist: ExecAllowlistEntry[];
safeBins: Set<string>;
cwd?: string;
platform?: string | null;
trustedSafeBinDirs?: ReadonlySet<string>;
skillBins?: Set<string>;
autoAllowSkills?: boolean;
@@ -156,6 +167,7 @@ export function evaluateExecAllowlist(params: {
allowlist: params.allowlist,
safeBins: params.safeBins,
cwd: params.cwd,
platform: params.platform,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills,
@@ -174,6 +186,7 @@ export function evaluateExecAllowlist(params: {
allowlist: params.allowlist,
safeBins: params.safeBins,
cwd: params.cwd,
platform: params.platform,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills,
@@ -231,6 +244,7 @@ export function evaluateShellAllowlist(params: {
allowlist: params.allowlist,
safeBins: params.safeBins,
cwd: params.cwd,
platform: params.platform,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills,
@@ -265,6 +279,7 @@ export function evaluateShellAllowlist(params: {
allowlist: params.allowlist,
safeBins: params.safeBins,
cwd: params.cwd,
platform: params.platform,
trustedSafeBinDirs: params.trustedSafeBinDirs,
skillBins: params.skillBins,
autoAllowSkills: params.autoAllowSkills,