Skip to content

Commit 7800626

Browse files
committed
🔒 fix(exec): preserve trusted command aliases
1 parent 958667e commit 7800626

2 files changed

Lines changed: 76 additions & 21 deletions

File tree

‎Sources/MenubucketCore/ExecService.swift‎

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -345,19 +345,15 @@ public final class ExecService {
345345
}
346346

347347
func matchesCommandIdentity(_ candidate: URL) -> Bool {
348-
if !command0.contains("/") {
349-
return candidate.resolvingSymlinksInPath().lastPathComponent == command0
350-
}
351-
352-
let declared: URL
353-
if command0.hasPrefix("./") || command0.hasPrefix("../") {
354-
let base = workingDirectory ?? URL(fileURLWithPath: fm.currentDirectoryPath)
355-
declared = base.appendingPathComponent(command0)
356-
} else {
357-
declared = URL(fileURLWithPath: (command0 as NSString).expandingTildeInPath)
358-
}
359-
return candidate.standardizedFileURL.resolvingSymlinksInPath().path
360-
== declared.standardizedFileURL.resolvingSymlinksInPath().path
348+
let trustedBareCommand = command0.contains("/")
349+
? nil
350+
: searchPATH(for: command0, executableURL: executableURL)
351+
return executableMatchesCommandIdentity(
352+
command0: command0,
353+
candidate: candidate,
354+
workingDirectory: workingDirectory,
355+
trustedBareCommand: trustedBareCommand
356+
)
361357
}
362358

363359
func authorizedExecutableURL(atPath path: String) -> URL? {
@@ -411,6 +407,33 @@ public final class ExecService {
411407
return searchPATH(for: command0, executableURL: executableURL)
412408
}
413409

410+
static func executableMatchesCommandIdentity(
411+
command0: String,
412+
candidate: URL,
413+
workingDirectory: URL?,
414+
trustedBareCommand: URL?
415+
) -> Bool {
416+
let canonicalCandidate = candidate.standardizedFileURL.resolvingSymlinksInPath()
417+
if !command0.contains("/") {
418+
guard candidate.lastPathComponent == command0 else { return false }
419+
if canonicalCandidate.lastPathComponent == command0 { return true }
420+
guard let trustedBareCommand else { return false }
421+
return canonicalCandidate.path
422+
== trustedBareCommand.standardizedFileURL.resolvingSymlinksInPath().path
423+
}
424+
425+
let declared: URL
426+
if command0.hasPrefix("./") || command0.hasPrefix("../") {
427+
let base = workingDirectory
428+
?? URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
429+
declared = base.appendingPathComponent(command0)
430+
} else {
431+
declared = URL(fileURLWithPath: (command0 as NSString).expandingTildeInPath)
432+
}
433+
return canonicalCandidate.path
434+
== declared.standardizedFileURL.resolvingSymlinksInPath().path
435+
}
436+
414437
private static func searchPATH(
415438
for name: String,
416439
executableURL: (String) -> URL?

‎Tests/MenubucketCoreTests/ExecServiceTests.swift‎

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,23 +49,55 @@ final class ExecServiceTests: XCTestCase {
4949
XCTAssertNil(resolved)
5050
}
5151

52-
func testDiscoveryAcceptsBareCommandSymlinkedToSameExecutableIdentity() throws {
52+
func testIdentityAcceptsTrustedPATHAliasWithDifferentCanonicalBasename() throws {
5353
let directory = FileManager.default.temporaryDirectory
5454
.appendingPathComponent(UUID().uuidString, isDirectory: true)
5555
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
5656
defer { try? FileManager.default.removeItem(at: directory) }
57-
let symlink = directory.appendingPathComponent("sh")
57+
let symlink = directory.appendingPathComponent("python3")
5858
try FileManager.default.createSymbolicLink(atPath: symlink.path, withDestinationPath: "/bin/sh")
59-
var searched: [String] = []
6059

61-
let resolved = ExecService.resolveExecutable(
60+
XCTAssertTrue(ExecService.executableMatchesCommandIdentity(
61+
command0: "python3",
62+
candidate: symlink,
63+
workingDirectory: nil,
64+
trustedBareCommand: URL(fileURLWithPath: "/bin/sh")
65+
))
66+
}
67+
68+
func testIdentityRejectsAliasThatDiffersFromTrustedPATHExecutable() throws {
69+
let directory = FileManager.default.temporaryDirectory
70+
.appendingPathComponent(UUID().uuidString, isDirectory: true)
71+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
72+
defer { try? FileManager.default.removeItem(at: directory) }
73+
let symlink = directory.appendingPathComponent("git")
74+
try FileManager.default.createSymbolicLink(atPath: symlink.path, withDestinationPath: "/bin/sh")
75+
76+
XCTAssertFalse(ExecService.executableMatchesCommandIdentity(
77+
command0: "git",
78+
candidate: symlink,
79+
workingDirectory: nil,
80+
trustedBareCommand: URL(fileURLWithPath: "/usr/bin/git")
81+
))
82+
}
83+
84+
func testIdentityRejectsCandidateWhosePresentedNameDiffers() {
85+
XCTAssertFalse(ExecService.executableMatchesCommandIdentity(
86+
command0: "git",
87+
candidate: URL(fileURLWithPath: "/bin/sh"),
88+
workingDirectory: nil,
89+
trustedBareCommand: URL(fileURLWithPath: "/bin/sh")
90+
))
91+
}
92+
93+
func testIdentityAcceptsOrdinarySameCanonicalBasename() {
94+
XCTAssertTrue(ExecService.executableMatchesCommandIdentity(
6295
command0: "sh",
63-
discover: [symlink.path],
96+
candidate: URL(fileURLWithPath: "/bin/sh"),
6497
workingDirectory: nil,
65-
searched: &searched
98+
trustedBareCommand: nil
99+
)
66100
)
67-
68-
XCTAssertEqual(try XCTUnwrap(resolved).path, symlink.path)
69101
}
70102

71103
func testDiscoveryRejectsCandidateForDifferentPathCommandIdentity() {

0 commit comments

Comments
 (0)