mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 20:08:26 +00:00
fix(exec): require explicit safe-bin profiles
This commit is contained in:
@@ -11,7 +11,6 @@ import {
|
||||
} from "./exec-approvals-analysis.js";
|
||||
import type { ExecAllowlistEntry } from "./exec-approvals.js";
|
||||
import {
|
||||
SAFE_BIN_GENERIC_PROFILE,
|
||||
SAFE_BIN_PROFILES,
|
||||
type SafeBinProfile,
|
||||
validateSafeBinArgv,
|
||||
@@ -41,7 +40,6 @@ export function isSafeBinUsage(params: {
|
||||
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.
|
||||
@@ -75,8 +73,10 @@ export function isSafeBinUsage(params: {
|
||||
}
|
||||
const argv = params.argv.slice(1);
|
||||
const safeBinProfiles = params.safeBinProfiles ?? SAFE_BIN_PROFILES;
|
||||
const genericSafeBinProfile = params.safeBinGenericProfile ?? SAFE_BIN_GENERIC_PROFILE;
|
||||
const profile = safeBinProfiles[execName] ?? genericSafeBinProfile;
|
||||
const profile = safeBinProfiles[execName];
|
||||
if (!profile) {
|
||||
return false;
|
||||
}
|
||||
return validateSafeBinArgv(argv, profile);
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ function evaluateSegments(
|
||||
params: {
|
||||
allowlist: ExecAllowlistEntry[];
|
||||
safeBins: Set<string>;
|
||||
safeBinProfiles?: Readonly<Record<string, SafeBinProfile>>;
|
||||
cwd?: string;
|
||||
platform?: string | null;
|
||||
trustedSafeBinDirs?: ReadonlySet<string>;
|
||||
@@ -122,6 +123,7 @@ function evaluateSegments(
|
||||
argv: segment.argv,
|
||||
resolution: segment.resolution,
|
||||
safeBins: params.safeBins,
|
||||
safeBinProfiles: params.safeBinProfiles,
|
||||
platform: params.platform,
|
||||
trustedSafeBinDirs: params.trustedSafeBinDirs,
|
||||
});
|
||||
@@ -147,6 +149,7 @@ export function evaluateExecAllowlist(params: {
|
||||
analysis: ExecCommandAnalysis;
|
||||
allowlist: ExecAllowlistEntry[];
|
||||
safeBins: Set<string>;
|
||||
safeBinProfiles?: Readonly<Record<string, SafeBinProfile>>;
|
||||
cwd?: string;
|
||||
platform?: string | null;
|
||||
trustedSafeBinDirs?: ReadonlySet<string>;
|
||||
@@ -165,6 +168,7 @@ export function evaluateExecAllowlist(params: {
|
||||
const result = evaluateSegments(chainSegments, {
|
||||
allowlist: params.allowlist,
|
||||
safeBins: params.safeBins,
|
||||
safeBinProfiles: params.safeBinProfiles,
|
||||
cwd: params.cwd,
|
||||
platform: params.platform,
|
||||
trustedSafeBinDirs: params.trustedSafeBinDirs,
|
||||
@@ -184,6 +188,7 @@ export function evaluateExecAllowlist(params: {
|
||||
const result = evaluateSegments(params.analysis.segments, {
|
||||
allowlist: params.allowlist,
|
||||
safeBins: params.safeBins,
|
||||
safeBinProfiles: params.safeBinProfiles,
|
||||
cwd: params.cwd,
|
||||
platform: params.platform,
|
||||
trustedSafeBinDirs: params.trustedSafeBinDirs,
|
||||
@@ -354,6 +359,7 @@ export function evaluateShellAllowlist(params: {
|
||||
command: string;
|
||||
allowlist: ExecAllowlistEntry[];
|
||||
safeBins: Set<string>;
|
||||
safeBinProfiles?: Readonly<Record<string, SafeBinProfile>>;
|
||||
cwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
trustedSafeBinDirs?: ReadonlySet<string>;
|
||||
@@ -384,6 +390,7 @@ export function evaluateShellAllowlist(params: {
|
||||
analysis,
|
||||
allowlist: params.allowlist,
|
||||
safeBins: params.safeBins,
|
||||
safeBinProfiles: params.safeBinProfiles,
|
||||
cwd: params.cwd,
|
||||
platform: params.platform,
|
||||
trustedSafeBinDirs: params.trustedSafeBinDirs,
|
||||
@@ -419,6 +426,7 @@ export function evaluateShellAllowlist(params: {
|
||||
analysis,
|
||||
allowlist: params.allowlist,
|
||||
safeBins: params.safeBins,
|
||||
safeBinProfiles: params.safeBinProfiles,
|
||||
cwd: params.cwd,
|
||||
platform: params.platform,
|
||||
trustedSafeBinDirs: params.trustedSafeBinDirs,
|
||||
|
||||
@@ -29,7 +29,11 @@ import {
|
||||
type ExecAllowlistEntry,
|
||||
type ExecApprovalsFile,
|
||||
} from "./exec-approvals.js";
|
||||
import { SAFE_BIN_PROFILE_FIXTURES, SAFE_BIN_PROFILES } from "./exec-safe-bin-policy.js";
|
||||
import {
|
||||
SAFE_BIN_PROFILE_FIXTURES,
|
||||
SAFE_BIN_PROFILES,
|
||||
resolveSafeBinProfiles,
|
||||
} from "./exec-safe-bin-policy.js";
|
||||
|
||||
function makePathEnv(binDir: string): NodeJS.ProcessEnv {
|
||||
if (process.platform !== "win32") {
|
||||
@@ -798,6 +802,53 @@ describe("exec approvals safe bins", () => {
|
||||
expect(defaults.has("grep")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not auto-allow unprofiled safe-bin entries", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const result = evaluateShellAllowlist({
|
||||
command: "python3 -c \"print('owned')\"",
|
||||
allowlist: [],
|
||||
safeBins: normalizeSafeBins(["python3"]),
|
||||
cwd: "/tmp",
|
||||
});
|
||||
expect(result.analysisOk).toBe(true);
|
||||
expect(result.allowlistSatisfied).toBe(false);
|
||||
});
|
||||
|
||||
it("allows caller-defined custom safe-bin profiles", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const safeBinProfiles = resolveSafeBinProfiles({
|
||||
echo: {
|
||||
maxPositional: 1,
|
||||
},
|
||||
});
|
||||
const allow = isSafeBinUsage({
|
||||
argv: ["echo", "hello"],
|
||||
resolution: {
|
||||
rawExecutable: "echo",
|
||||
resolvedPath: "/bin/echo",
|
||||
executableName: "echo",
|
||||
},
|
||||
safeBins: normalizeSafeBins(["echo"]),
|
||||
safeBinProfiles,
|
||||
});
|
||||
const deny = isSafeBinUsage({
|
||||
argv: ["echo", "hello", "world"],
|
||||
resolution: {
|
||||
rawExecutable: "echo",
|
||||
resolvedPath: "/bin/echo",
|
||||
executableName: "echo",
|
||||
},
|
||||
safeBins: normalizeSafeBins(["echo"]),
|
||||
safeBinProfiles,
|
||||
});
|
||||
expect(allow).toBe(true);
|
||||
expect(deny).toBe(false);
|
||||
});
|
||||
|
||||
it("blocks sort output flags independent of file existence", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
|
||||
@@ -37,6 +37,8 @@ export type SafeBinProfileFixture = {
|
||||
deniedFlags?: readonly string[];
|
||||
};
|
||||
|
||||
export type SafeBinProfileFixtures = Readonly<Record<string, SafeBinProfileFixture>>;
|
||||
|
||||
const NO_FLAGS: ReadonlySet<string> = new Set();
|
||||
|
||||
const toFlagSet = (flags?: readonly string[]): ReadonlySet<string> => {
|
||||
@@ -63,8 +65,6 @@ function compileSafeBinProfiles(
|
||||
) as Record<string, SafeBinProfile>;
|
||||
}
|
||||
|
||||
export const SAFE_BIN_GENERIC_PROFILE_FIXTURE: SafeBinProfileFixture = {};
|
||||
|
||||
export const SAFE_BIN_PROFILE_FIXTURES: Record<string, SafeBinProfileFixture> = {
|
||||
jq: {
|
||||
maxPositional: 1,
|
||||
@@ -184,11 +184,49 @@ export const SAFE_BIN_PROFILE_FIXTURES: Record<string, SafeBinProfileFixture> =
|
||||
},
|
||||
};
|
||||
|
||||
export const SAFE_BIN_GENERIC_PROFILE = compileSafeBinProfile(SAFE_BIN_GENERIC_PROFILE_FIXTURE);
|
||||
|
||||
export const SAFE_BIN_PROFILES: Record<string, SafeBinProfile> =
|
||||
compileSafeBinProfiles(SAFE_BIN_PROFILE_FIXTURES);
|
||||
|
||||
function normalizeSafeBinProfileName(raw: string): string | null {
|
||||
const name = raw.trim().toLowerCase();
|
||||
return name.length > 0 ? name : null;
|
||||
}
|
||||
|
||||
function normalizeSafeBinProfileFixtures(
|
||||
fixtures?: SafeBinProfileFixtures | null,
|
||||
): Record<string, SafeBinProfileFixture> {
|
||||
const normalized: Record<string, SafeBinProfileFixture> = {};
|
||||
if (!fixtures) {
|
||||
return normalized;
|
||||
}
|
||||
for (const [rawName, fixture] of Object.entries(fixtures)) {
|
||||
const name = normalizeSafeBinProfileName(rawName);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
normalized[name] = {
|
||||
minPositional: fixture.minPositional,
|
||||
maxPositional: fixture.maxPositional,
|
||||
allowedValueFlags: fixture.allowedValueFlags,
|
||||
deniedFlags: fixture.deniedFlags,
|
||||
};
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function resolveSafeBinProfiles(
|
||||
fixtures?: SafeBinProfileFixtures | null,
|
||||
): Record<string, SafeBinProfile> {
|
||||
const normalizedFixtures = normalizeSafeBinProfileFixtures(fixtures);
|
||||
if (Object.keys(normalizedFixtures).length === 0) {
|
||||
return SAFE_BIN_PROFILES;
|
||||
}
|
||||
return {
|
||||
...SAFE_BIN_PROFILES,
|
||||
...compileSafeBinProfiles(normalizedFixtures),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveSafeBinDeniedFlags(
|
||||
fixtures: Readonly<Record<string, SafeBinProfileFixture>> = SAFE_BIN_PROFILE_FIXTURES,
|
||||
): Record<string, string[]> {
|
||||
|
||||
Reference in New Issue
Block a user