fix(macos): fail closed on env-modified shell wrappers

This commit is contained in:
Nimrod Gutman 2026-03-19 15:31:26 +02:00
parent e4b70ea497
commit 1d45febea8
2 changed files with 31 additions and 0 deletions

View File

@ -77,6 +77,11 @@ struct ExecCommandResolution {
guard depth <= ExecWrapperResolution.maxWrapperDepth, !command.isEmpty else {
return []
}
if ExecWrapperResolution.hasEnvManipulationBeforeShellWrapper(command) {
// Fail closed for semantic env wrappers that can alter shell lookup
// semantics before we would analyze inner shell payloads.
return []
}
let shell = ExecShellWrapperParser.extract(command: command, rawCommand: rawCommand)
if shell.isWrapper {
@ -194,6 +199,12 @@ struct ExecCommandResolution {
guard depth <= Self.maxAllowAlwaysTraversalDepth, !command.isEmpty else {
return
}
if ExecWrapperResolution.hasEnvManipulationBeforeShellWrapper(command) {
// Mirror the conservative node-host policy for env-modified shell
// launches: require explicit approval each time instead of persisting
// an inner-executable pattern that the modified environment can subvert.
return
}
// Allow-always persistence intentionally peels known dispatch wrappers
// directly so approvals stay scoped to the launched executable instead of

View File

@ -382,6 +382,17 @@ struct ExecAllowlistTests {
#expect(resolutions[0].executableName == "env")
}
@Test func `resolve for allowlist fails closed on env manipulation before shell wrapper`() {
let command = ["/usr/bin/env", "PATH=/tmp", "/bin/sh", "-lc", "whoami"]
let resolutions = ExecCommandResolution.resolveForAllowlist(
command: command,
rawCommand: nil,
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(resolutions.isEmpty)
}
@Test func `resolve for allowlist preserves env wrapper with modifiers`() {
let command = ["/usr/bin/env", "FOO=bar", "/usr/bin/printf", "ok"]
let resolutions = ExecCommandResolution.resolveForAllowlist(
@ -435,6 +446,15 @@ struct ExecAllowlistTests {
#expect(patterns == ["/usr/bin/printf"])
}
@Test func `allow always patterns fail closed on env manipulation before shell wrapper`() {
let patterns = ExecCommandResolution.resolveAllowAlwaysPatterns(
command: ["/usr/bin/env", "PATH=/tmp", "/bin/sh", "-lc", "whoami"],
cwd: nil,
env: ["PATH": "/usr/bin:/bin"])
#expect(patterns.isEmpty)
}
@Test func `allow always patterns unwrap dispatch wrappers before shell wrappers`() throws {
let tmp = try makeTempDirForTests()
let whoami = tmp.appendingPathComponent("whoami")