mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
fix(exec): harden safe-bin trust and add explicit trusted dirs
This commit is contained in:
@@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Workspace guard: map sandbox container-workdir file-tool paths (for example `/workspace/...` and `file:///workspace/...`) to host workspace roots before workspace-only validation, preventing false `Path escapes sandbox root` rejections for sandbox file tools. (#9560)
|
||||
- Gateway/Exec approvals: expire approval requests immediately when no approval-capable gateway clients are connected and no forwarding targets are available, avoiding delayed approvals after restarts/offline approver windows. (#22144)
|
||||
- Node/macOS exec host: default headless macOS node `system.run` to local execution and only route through the companion app when `OPENCLAW_NODE_EXEC_HOST=app` is explicitly set, avoiding companion-app filesystem namespace mismatches during exec. (#23547)
|
||||
- Security/Exec: stop trusting `PATH`-derived directories for safe-bin allowlist checks, add explicit `tools.exec.safeBinTrustedDirs`, and pin safe-bin shell execution to resolved absolute executable paths to prevent binary-shadowing approval bypasses. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
- Slack/Threading: sessions: keep parent-session forking and thread-history context active beyond first turn by removing first-turn-only gates in session init, thread-history fetch, and reply prompt context injection. (#23843, #23090) Thanks @vincentkoc and @Taskle.
|
||||
- Slack/Threading: respect `replyToMode` when Slack auto-populates top-level `thread_ts`, and ignore inline `replyToId` directive tags when `replyToMode` is `off` so thread forcing stays disabled unless explicitly configured. (#23839, #23320, #23513) Thanks @vincentkoc and @dorukardahan.
|
||||
- Slack/Extension: forward `message read` `threadId` to `readMessages` and use delivery-context `threadId` as outbound `thread_ts` fallback so extension replies/reads stay in the correct Slack thread. (#22216, #22485, #23836) Thanks @vincentkoc, @lan17 and @dorukardahan.
|
||||
|
||||
@@ -148,8 +148,8 @@ Denied flags by safe-bin profile:
|
||||
Safe bins also force argv tokens to be treated as **literal text** at execution time (no globbing
|
||||
and no `$VARS` expansion) for stdin-only segments, so patterns like `*` or `$HOME/...` cannot be
|
||||
used to smuggle file reads.
|
||||
Safe bins must also resolve from trusted binary directories (system defaults plus the gateway
|
||||
process `PATH` at startup). This blocks request-scoped PATH hijacking attempts.
|
||||
Safe bins must also resolve from trusted binary directories (system defaults plus optional
|
||||
`tools.exec.safeBinTrustedDirs`). `PATH` entries are never auto-trusted.
|
||||
Shell chaining and redirections are not auto-allowed in allowlist mode.
|
||||
|
||||
Shell chaining (`&&`, `||`, `;`) is allowed when every top-level segment satisfies the allowlist
|
||||
@@ -182,6 +182,7 @@ rejected so file operands cannot be smuggled as ambiguous positionals.
|
||||
Configuration location:
|
||||
|
||||
- `safeBins` comes from config (`tools.exec.safeBins` or per-agent `agents.list[].tools.exec.safeBins`).
|
||||
- `safeBinTrustedDirs` comes from config (`tools.exec.safeBinTrustedDirs` or per-agent `agents.list[].tools.exec.safeBinTrustedDirs`).
|
||||
- `safeBinProfiles` comes from config (`tools.exec.safeBinProfiles` or per-agent `agents.list[].tools.exec.safeBinProfiles`). Per-agent profile keys override global keys.
|
||||
- allowlist entries live in host-local `~/.openclaw/exec-approvals.json` under `agents.<id>.allowlist` (or via Control UI / `openclaw approvals allowlist ...`).
|
||||
- `openclaw security audit` warns with `tools.exec.safe_bins_interpreter_unprofiled` when interpreter/runtime bins appear in `safeBins` without explicit profiles.
|
||||
|
||||
@@ -55,6 +55,7 @@ Notes:
|
||||
- `tools.exec.node` (default: unset)
|
||||
- `tools.exec.pathPrepend`: list of directories to prepend to `PATH` for exec runs (gateway + sandbox only).
|
||||
- `tools.exec.safeBins`: stdin-only safe binaries that can run without explicit allowlist entries. For behavior details, see [Safe bins](/tools/exec-approvals#safe-bins-stdin-only).
|
||||
- `tools.exec.safeBinTrustedDirs`: additional explicit directories trusted for `safeBins` path checks. `PATH` entries are never auto-trusted.
|
||||
- `tools.exec.safeBinProfiles`: optional custom argv policy per safe bin (`minPositional`, `maxPositional`, `allowedValueFlags`, `deniedFlags`).
|
||||
|
||||
Example:
|
||||
@@ -130,6 +131,7 @@ Redirections remain unsupported.
|
||||
Use the two controls for different jobs:
|
||||
|
||||
- `tools.exec.safeBins`: small, stdin-only stream filters.
|
||||
- `tools.exec.safeBinTrustedDirs`: explicit extra trusted directories for safe-bin executable paths.
|
||||
- `tools.exec.safeBinProfiles`: explicit argv policy for custom safe bins.
|
||||
- allowlist: explicit trust for executable paths.
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export type ExecToolDefaults = {
|
||||
node?: string;
|
||||
pathPrepend?: string[];
|
||||
safeBins?: string[];
|
||||
safeBinTrustedDirs?: string[];
|
||||
safeBinProfiles?: Record<string, SafeBinProfileFixture>;
|
||||
agentId?: string;
|
||||
backgroundMs?: number;
|
||||
|
||||
@@ -172,6 +172,7 @@ export function createExecTool(
|
||||
} = resolveExecSafeBinRuntimePolicy({
|
||||
local: {
|
||||
safeBins: defaults?.safeBins,
|
||||
safeBinTrustedDirs: defaults?.safeBinTrustedDirs,
|
||||
safeBinProfiles: defaults?.safeBinProfiles,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -106,6 +106,7 @@ function resolveExecConfig(params: { cfg?: OpenClawConfig; agentId?: string }) {
|
||||
node: agentExec?.node ?? globalExec?.node,
|
||||
pathPrepend: agentExec?.pathPrepend ?? globalExec?.pathPrepend,
|
||||
safeBins: agentExec?.safeBins ?? globalExec?.safeBins,
|
||||
safeBinTrustedDirs: agentExec?.safeBinTrustedDirs ?? globalExec?.safeBinTrustedDirs,
|
||||
safeBinProfiles: resolveMergedSafeBinProfileFixtures({
|
||||
global: globalExec,
|
||||
local: agentExec,
|
||||
@@ -373,6 +374,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
node: options?.exec?.node ?? execConfig.node,
|
||||
pathPrepend: options?.exec?.pathPrepend ?? execConfig.pathPrepend,
|
||||
safeBins: options?.exec?.safeBins ?? execConfig.safeBins,
|
||||
safeBinTrustedDirs: options?.exec?.safeBinTrustedDirs ?? execConfig.safeBinTrustedDirs,
|
||||
safeBinProfiles: options?.exec?.safeBinProfiles ?? execConfig.safeBinProfiles,
|
||||
agentId,
|
||||
cwd: workspaceRoot,
|
||||
|
||||
@@ -78,7 +78,7 @@ describe("config io paths", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes safeBinProfiles at config load time", async () => {
|
||||
it("normalizes safe-bin config entries at config load time", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
@@ -89,6 +89,7 @@ describe("config io paths", () => {
|
||||
{
|
||||
tools: {
|
||||
exec: {
|
||||
safeBinTrustedDirs: [" /custom/bin ", "", "/custom/bin", "/agent/bin"],
|
||||
safeBinProfiles: {
|
||||
" MyFilter ": {
|
||||
allowedValueFlags: ["--limit", " --limit ", ""],
|
||||
@@ -102,6 +103,7 @@ describe("config io paths", () => {
|
||||
id: "ops",
|
||||
tools: {
|
||||
exec: {
|
||||
safeBinTrustedDirs: [" /ops/bin ", "/ops/bin"],
|
||||
safeBinProfiles: {
|
||||
" Custom ": {
|
||||
deniedFlags: ["-f", " -f ", ""],
|
||||
@@ -126,11 +128,13 @@ describe("config io paths", () => {
|
||||
allowedValueFlags: ["--limit"],
|
||||
},
|
||||
});
|
||||
expect(cfg.tools?.exec?.safeBinTrustedDirs).toEqual(["/custom/bin", "/agent/bin"]);
|
||||
expect(cfg.agents?.list?.[0]?.tools?.exec?.safeBinProfiles).toEqual({
|
||||
custom: {
|
||||
deniedFlags: ["-f"],
|
||||
},
|
||||
});
|
||||
expect(cfg.agents?.list?.[0]?.tools?.exec?.safeBinTrustedDirs).toEqual(["/ops/bin"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -557,11 +557,22 @@ function maybeLoadDotEnvForConfig(env: NodeJS.ProcessEnv): void {
|
||||
}
|
||||
|
||||
function normalizeExecSafeBinProfilesInConfig(cfg: OpenClawConfig): void {
|
||||
const normalizeTrustedDirs = (entries?: readonly string[]) => {
|
||||
if (!Array.isArray(entries)) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = entries.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
||||
return normalized.length > 0 ? Array.from(new Set(normalized)) : undefined;
|
||||
};
|
||||
|
||||
const normalizeExec = (exec: unknown) => {
|
||||
if (!exec || typeof exec !== "object" || Array.isArray(exec)) {
|
||||
return;
|
||||
}
|
||||
const typedExec = exec as { safeBinProfiles?: Record<string, unknown> };
|
||||
const typedExec = exec as {
|
||||
safeBinProfiles?: Record<string, unknown>;
|
||||
safeBinTrustedDirs?: string[];
|
||||
};
|
||||
const normalized = normalizeSafeBinProfileFixtures(
|
||||
typedExec.safeBinProfiles as Record<
|
||||
string,
|
||||
@@ -574,6 +585,7 @@ function normalizeExecSafeBinProfilesInConfig(cfg: OpenClawConfig): void {
|
||||
>,
|
||||
);
|
||||
typedExec.safeBinProfiles = Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
typedExec.safeBinTrustedDirs = normalizeTrustedDirs(typedExec.safeBinTrustedDirs);
|
||||
};
|
||||
|
||||
normalizeExec(cfg.tools?.exec);
|
||||
|
||||
@@ -418,6 +418,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"tools.exec.pathPrepend": "Directories to prepend to PATH for exec runs (gateway/sandbox).",
|
||||
"tools.exec.safeBins":
|
||||
"Allow stdin-only safe binaries to run without explicit allowlist entries.",
|
||||
"tools.exec.safeBinTrustedDirs":
|
||||
"Additional explicit directories trusted for safe-bin path checks (PATH entries are never auto-trusted).",
|
||||
"tools.exec.safeBinProfiles":
|
||||
"Optional per-binary safe-bin profiles (positional limits + allowed/denied flags).",
|
||||
"tools.profile":
|
||||
|
||||
@@ -183,6 +183,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"tools.sandbox.tools": "Sandbox Tool Allow/Deny Policy",
|
||||
"tools.exec.pathPrepend": "Exec PATH Prepend",
|
||||
"tools.exec.safeBins": "Exec Safe Bins",
|
||||
"tools.exec.safeBinTrustedDirs": "Exec Safe Bin Trusted Dirs",
|
||||
"tools.exec.safeBinProfiles": "Exec Safe Bin Profiles",
|
||||
approvals: "Approvals",
|
||||
"approvals.exec": "Exec Approval Forwarding",
|
||||
|
||||
@@ -227,6 +227,8 @@ export type ExecToolConfig = {
|
||||
pathPrepend?: string[];
|
||||
/** Safe stdin-only binaries that can run without allowlist entries. */
|
||||
safeBins?: string[];
|
||||
/** Extra explicit directories trusted for safeBins path checks (never derived from PATH). */
|
||||
safeBinTrustedDirs?: string[];
|
||||
/** Optional custom safe-bin profiles for entries in tools.exec.safeBins. */
|
||||
safeBinProfiles?: Record<string, SafeBinProfileFixture>;
|
||||
/** Default time (ms) before an exec command auto-backgrounds. */
|
||||
|
||||
@@ -353,6 +353,7 @@ const ToolExecBaseShape = {
|
||||
node: z.string().optional(),
|
||||
pathPrepend: z.array(z.string()).optional(),
|
||||
safeBins: z.array(z.string()).optional(),
|
||||
safeBinTrustedDirs: z.array(z.string()).optional(),
|
||||
safeBinProfiles: z.record(z.string(), ToolExecSafeBinProfileSchema).optional(),
|
||||
backgroundMs: z.number().int().positive().optional(),
|
||||
timeoutSec: z.number().int().positive().optional(),
|
||||
|
||||
@@ -882,6 +882,15 @@ function renderQuotedArgv(argv: string[]): string {
|
||||
return argv.map((token) => shellEscapeSingleArg(token)).join(" ");
|
||||
}
|
||||
|
||||
function renderSafeBinSegmentArgv(segment: ExecCommandSegment): string {
|
||||
if (segment.argv.length === 0) {
|
||||
return "";
|
||||
}
|
||||
const resolvedExecutable = segment.resolution?.resolvedPath?.trim();
|
||||
const argv = resolvedExecutable ? [resolvedExecutable, ...segment.argv.slice(1)] : segment.argv;
|
||||
return renderQuotedArgv(argv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds a shell command and selectively single-quotes argv tokens for segments that
|
||||
* must be treated as literal (safeBins hardening) while preserving the rest of the
|
||||
@@ -920,7 +929,7 @@ export function buildSafeBinsShellCommand(params: {
|
||||
return { ok: false, reason: "segment mapping failed" };
|
||||
}
|
||||
const needsLiteral = by === "safeBins";
|
||||
rendered.push(needsLiteral ? renderQuotedArgv(seg.argv) : raw.trim());
|
||||
rendered.push(needsLiteral ? renderSafeBinSegmentArgv(seg) : raw.trim());
|
||||
segIndex += 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -195,8 +195,8 @@ describe("exec approvals safe shell command builder", () => {
|
||||
expect(res.ok).toBe(true);
|
||||
// Preserve non-safeBins segment raw (glob stays unquoted)
|
||||
expect(res.command).toContain("rg foo src/*.ts");
|
||||
// SafeBins segment is fully quoted
|
||||
expect(res.command).toContain("'head' '-n' '5'");
|
||||
// SafeBins segment is fully quoted and pinned to its resolved absolute path.
|
||||
expect(res.command).toMatch(/'[^']*\/head' '-n' '5'/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -936,6 +936,30 @@ describe("exec approvals safe bins", () => {
|
||||
});
|
||||
expect(allowed.allowlistSatisfied).toBe(true);
|
||||
});
|
||||
|
||||
it("does not auto-trust PATH-shadowed safe bins without explicit trusted dirs", () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const tmp = makeTempDir();
|
||||
const fakeDir = path.join(tmp, "fake-bin");
|
||||
fs.mkdirSync(fakeDir, { recursive: true });
|
||||
const fakeHead = path.join(fakeDir, "head");
|
||||
fs.writeFileSync(fakeHead, "#!/bin/sh\nexit 0\n");
|
||||
fs.chmodSync(fakeHead, 0o755);
|
||||
|
||||
const result = evaluateShellAllowlist({
|
||||
command: "head -n 1",
|
||||
allowlist: [],
|
||||
safeBins: normalizeSafeBins(["head"]),
|
||||
env: makePathEnv(fakeDir),
|
||||
cwd: tmp,
|
||||
});
|
||||
expect(result.analysisOk).toBe(true);
|
||||
expect(result.allowlistSatisfied).toBe(false);
|
||||
expect(result.segmentSatisfiedBy).toEqual([null]);
|
||||
expect(result.segments[0]?.resolution?.resolvedPath).toBe(fakeHead);
|
||||
});
|
||||
});
|
||||
|
||||
describe("exec approvals allowlist evaluation", () => {
|
||||
|
||||
@@ -70,4 +70,18 @@ describe("exec safe-bin runtime policy", () => {
|
||||
expect(policy.unprofiledSafeBins).toEqual(["python3"]);
|
||||
expect(policy.unprofiledInterpreterSafeBins).toEqual(["python3"]);
|
||||
});
|
||||
|
||||
it("merges explicit safe-bin trusted dirs from global and local config", () => {
|
||||
const policy = resolveExecSafeBinRuntimePolicy({
|
||||
global: {
|
||||
safeBinTrustedDirs: [" /custom/bin ", "/custom/bin"],
|
||||
},
|
||||
local: {
|
||||
safeBinTrustedDirs: ["/agent/bin"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(policy.trustedSafeBinDirs.has("/custom/bin")).toBe(true);
|
||||
expect(policy.trustedSafeBinDirs.has("/agent/bin")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getTrustedSafeBinDirs } from "./exec-safe-bin-trust.js";
|
||||
export type ExecSafeBinConfigScope = {
|
||||
safeBins?: string[] | null;
|
||||
safeBinProfiles?: SafeBinProfileFixtures | null;
|
||||
safeBinTrustedDirs?: string[] | null;
|
||||
};
|
||||
|
||||
const INTERPRETER_LIKE_SAFE_BINS = new Set([
|
||||
@@ -78,6 +79,14 @@ export function listInterpreterLikeSafeBins(entries: Iterable<string>): string[]
|
||||
.toSorted();
|
||||
}
|
||||
|
||||
function normalizeTrustedDirs(entries?: string[] | null): string[] {
|
||||
if (!Array.isArray(entries)) {
|
||||
return [];
|
||||
}
|
||||
const normalized = entries.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
||||
return Array.from(new Set(normalized));
|
||||
}
|
||||
|
||||
export function resolveMergedSafeBinProfileFixtures(params: {
|
||||
global?: ExecSafeBinConfigScope | null;
|
||||
local?: ExecSafeBinConfigScope | null;
|
||||
@@ -96,7 +105,6 @@ export function resolveMergedSafeBinProfileFixtures(params: {
|
||||
export function resolveExecSafeBinRuntimePolicy(params: {
|
||||
global?: ExecSafeBinConfigScope | null;
|
||||
local?: ExecSafeBinConfigScope | null;
|
||||
pathEnv?: string | null;
|
||||
}): {
|
||||
safeBins: Set<string>;
|
||||
safeBinProfiles: Readonly<Record<string, SafeBinProfile>>;
|
||||
@@ -114,9 +122,12 @@ export function resolveExecSafeBinRuntimePolicy(params: {
|
||||
const unprofiledSafeBins = Array.from(safeBins)
|
||||
.filter((entry) => !safeBinProfiles[entry])
|
||||
.toSorted();
|
||||
const trustedSafeBinDirs = params.pathEnv
|
||||
? getTrustedSafeBinDirs({ pathEnv: params.pathEnv })
|
||||
: getTrustedSafeBinDirs();
|
||||
const trustedSafeBinDirs = getTrustedSafeBinDirs({
|
||||
extraDirs: [
|
||||
...normalizeTrustedDirs(params.global?.safeBinTrustedDirs),
|
||||
...normalizeTrustedDirs(params.local?.safeBinTrustedDirs),
|
||||
],
|
||||
});
|
||||
return {
|
||||
safeBins,
|
||||
safeBinProfiles,
|
||||
|
||||
@@ -8,11 +8,10 @@ import {
|
||||
} from "./exec-safe-bin-trust.js";
|
||||
|
||||
describe("exec safe bin trust", () => {
|
||||
it("builds trusted dirs from defaults and injected PATH", () => {
|
||||
it("builds trusted dirs from defaults and explicit extra dirs", () => {
|
||||
const dirs = buildTrustedSafeBinDirs({
|
||||
pathEnv: "/custom/bin:/alt/bin:/custom/bin",
|
||||
delimiter: ":",
|
||||
baseDirs: ["/usr/bin"],
|
||||
extraDirs: ["/custom/bin", "/alt/bin", "/custom/bin"],
|
||||
});
|
||||
|
||||
expect(dirs.has(path.resolve("/usr/bin"))).toBe(true);
|
||||
@@ -21,19 +20,16 @@ describe("exec safe bin trust", () => {
|
||||
expect(dirs.size).toBe(3);
|
||||
});
|
||||
|
||||
it("memoizes trusted dirs per PATH snapshot", () => {
|
||||
it("memoizes trusted dirs per explicit trusted-dir snapshot", () => {
|
||||
const a = getTrustedSafeBinDirs({
|
||||
pathEnv: "/first/bin",
|
||||
delimiter: ":",
|
||||
extraDirs: ["/first/bin"],
|
||||
refresh: true,
|
||||
});
|
||||
const b = getTrustedSafeBinDirs({
|
||||
pathEnv: "/first/bin",
|
||||
delimiter: ":",
|
||||
extraDirs: ["/first/bin"],
|
||||
});
|
||||
const c = getTrustedSafeBinDirs({
|
||||
pathEnv: "/second/bin",
|
||||
delimiter: ":",
|
||||
extraDirs: ["/second/bin"],
|
||||
});
|
||||
|
||||
expect(a).toBe(b);
|
||||
@@ -56,14 +52,12 @@ describe("exec safe bin trust", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("uses startup PATH snapshot when pathEnv is omitted", () => {
|
||||
it("does not trust PATH entries by default", () => {
|
||||
const injected = `/tmp/openclaw-path-injected-${Date.now()}`;
|
||||
const initial = getTrustedSafeBinDirs({ refresh: true });
|
||||
|
||||
withEnv({ PATH: `${injected}${path.delimiter}${process.env.PATH ?? ""}` }, () => {
|
||||
const refreshed = getTrustedSafeBinDirs({ refresh: true });
|
||||
expect(refreshed.has(path.resolve(injected))).toBe(false);
|
||||
expect([...refreshed].toSorted()).toEqual([...initial].toSorted());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,16 +11,13 @@ const DEFAULT_SAFE_BIN_TRUSTED_DIRS = [
|
||||
];
|
||||
|
||||
type TrustedSafeBinDirsParams = {
|
||||
pathEnv?: string | null;
|
||||
delimiter?: string;
|
||||
baseDirs?: readonly string[];
|
||||
extraDirs?: readonly string[];
|
||||
};
|
||||
|
||||
type TrustedSafeBinPathParams = {
|
||||
resolvedPath: string;
|
||||
trustedDirs?: ReadonlySet<string>;
|
||||
pathEnv?: string | null;
|
||||
delimiter?: string;
|
||||
};
|
||||
|
||||
type TrustedSafeBinCache = {
|
||||
@@ -29,7 +26,6 @@ type TrustedSafeBinCache = {
|
||||
};
|
||||
|
||||
let trustedSafeBinCache: TrustedSafeBinCache | null = null;
|
||||
const STARTUP_PATH_ENV = process.env.PATH ?? process.env.Path ?? "";
|
||||
|
||||
function normalizeTrustedDir(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
@@ -39,64 +35,54 @@ function normalizeTrustedDir(value: string): string | null {
|
||||
return path.resolve(trimmed);
|
||||
}
|
||||
|
||||
function buildTrustedSafeBinCacheKey(pathEnv: string, delimiter: string): string {
|
||||
return `${delimiter}\u0000${pathEnv}`;
|
||||
function buildTrustedSafeBinCacheKey(params: {
|
||||
baseDirs: readonly string[];
|
||||
extraDirs: readonly string[];
|
||||
}): string {
|
||||
return `${params.baseDirs.join("\u0001")}\u0000${params.extraDirs.join("\u0001")}`;
|
||||
}
|
||||
|
||||
export function buildTrustedSafeBinDirs(params: TrustedSafeBinDirsParams = {}): Set<string> {
|
||||
const delimiter = params.delimiter ?? path.delimiter;
|
||||
const pathEnv = params.pathEnv ?? "";
|
||||
const baseDirs = params.baseDirs ?? DEFAULT_SAFE_BIN_TRUSTED_DIRS;
|
||||
const extraDirs = params.extraDirs ?? [];
|
||||
const trusted = new Set<string>();
|
||||
|
||||
for (const entry of baseDirs) {
|
||||
// Trust is explicit only. Do not derive from PATH, which is user/environment controlled.
|
||||
for (const entry of [...baseDirs, ...extraDirs]) {
|
||||
const normalized = normalizeTrustedDir(entry);
|
||||
if (normalized) {
|
||||
trusted.add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
const pathEntries = pathEnv
|
||||
.split(delimiter)
|
||||
.map((entry) => normalizeTrustedDir(entry))
|
||||
.filter((entry): entry is string => Boolean(entry));
|
||||
for (const entry of pathEntries) {
|
||||
trusted.add(entry);
|
||||
}
|
||||
|
||||
return trusted;
|
||||
}
|
||||
|
||||
export function getTrustedSafeBinDirs(
|
||||
params: {
|
||||
pathEnv?: string | null;
|
||||
delimiter?: string;
|
||||
baseDirs?: readonly string[];
|
||||
extraDirs?: readonly string[];
|
||||
refresh?: boolean;
|
||||
} = {},
|
||||
): Set<string> {
|
||||
const delimiter = params.delimiter ?? path.delimiter;
|
||||
const pathEnv = params.pathEnv ?? STARTUP_PATH_ENV;
|
||||
const key = buildTrustedSafeBinCacheKey(pathEnv, delimiter);
|
||||
const baseDirs = params.baseDirs ?? DEFAULT_SAFE_BIN_TRUSTED_DIRS;
|
||||
const extraDirs = params.extraDirs ?? [];
|
||||
const key = buildTrustedSafeBinCacheKey({ baseDirs, extraDirs });
|
||||
|
||||
if (!params.refresh && trustedSafeBinCache?.key === key) {
|
||||
return trustedSafeBinCache.dirs;
|
||||
}
|
||||
|
||||
const dirs = buildTrustedSafeBinDirs({
|
||||
pathEnv,
|
||||
delimiter,
|
||||
baseDirs,
|
||||
extraDirs,
|
||||
});
|
||||
trustedSafeBinCache = { key, dirs };
|
||||
return dirs;
|
||||
}
|
||||
|
||||
export function isTrustedSafeBinPath(params: TrustedSafeBinPathParams): boolean {
|
||||
const trustedDirs =
|
||||
params.trustedDirs ??
|
||||
getTrustedSafeBinDirs({
|
||||
pathEnv: params.pathEnv,
|
||||
delimiter: params.delimiter,
|
||||
});
|
||||
const trustedDirs = params.trustedDirs ?? getTrustedSafeBinDirs();
|
||||
const resolvedDir = path.dirname(path.resolve(params.resolvedPath));
|
||||
return trustedDirs.has(resolvedDir);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user