Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 74 additions & 20 deletions src/Fantomas.Client/FantomasToolLocator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,45 @@

let private isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)

/// Validates that an executable path is safe to execute
/// Prevents arbitrary code execution by ensuring the file:
/// 1. Exists
/// 2. Is named 'fantomas' or 'fantomas-tool' (with optional .exe)
/// 3. Is not in suspicious locations (temp directories, user input paths)
let private validateExecutablePath (path: string) : bool =
try
if String.IsNullOrWhiteSpace path then
false
else
let fileName = Path.GetFileName(path).ToLowerInvariant()
let directory = Path.GetDirectoryName path

// Only allow known fantomas executable names
let validNames =
[ "fantomas"; "fantomas.exe"; "fantomas-tool"; "fantomas-tool.exe" ]

let isValidName = List.contains fileName validNames

// Block suspicious directories
let suspiciousPatterns =
[ Path.GetTempPath().ToLowerInvariant()
Environment.GetFolderPath(Environment.SpecialFolder.InternetCache).ToLowerInvariant()
"\\temp\\"
"/tmp/"
"\\downloads\\"
"/downloads/" ]

let isSuspiciousLocation =
suspiciousPatterns
|> List.exists (fun pattern ->
not (String.IsNullOrEmpty pattern)
&& directory.ToLowerInvariant().Contains pattern)

// File must exist and not be in suspicious location
isValidName && File.Exists path && not isSuspiciousLocation
with _ ->
false

// Find an executable fantomas file on the PATH
let private fantomasVersionOnPath () : (FantomasExecutableFile * FantomasVersion) option =
let fantomasExecutableOnPathOpt =
Expand All @@ -149,26 +188,30 @@

fantomasExecutableOnPathOpt
|> Option.bind (fun fantomasExecutablePath ->
let processStart = ProcessStartInfo(fantomasExecutablePath)
processStart.Arguments <- "--version"
processStart.RedirectStandardOutput <- true
processStart.CreateNoWindow <- true
processStart.RedirectStandardOutput <- true
processStart.RedirectStandardError <- true
processStart.UseShellExecute <- false

match startProcess processStart with
| Ok p ->
p.WaitForExit()
let stdOut = p.StandardOutput.ReadToEnd()

stdOut
|> Option.ofObj
|> Option.map (fun s ->
let version = s.ToLowerInvariant().Replace("fantomas", String.Empty).Trim()
FantomasExecutableFile(fantomasExecutablePath), FantomasVersion(version))
| Error(ProcessStartError.ExecutableFileNotFound _)
| Error(ProcessStartError.UnExpectedException _) -> None)
// SECURITY: Validate executable path before executing
if not (validateExecutablePath fantomasExecutablePath) then
None
else
let processStart = ProcessStartInfo(fantomasExecutablePath)
processStart.Arguments <- "--version"
processStart.RedirectStandardOutput <- true
processStart.CreateNoWindow <- true
processStart.RedirectStandardOutput <- true
processStart.RedirectStandardError <- true
processStart.UseShellExecute <- false

match startProcess processStart with
| Ok p ->
p.WaitForExit()
let stdOut = p.StandardOutput.ReadToEnd()

stdOut
|> Option.ofObj
|> Option.map (fun s ->
let version = s.ToLowerInvariant().Replace("fantomas", String.Empty).Trim()
FantomasExecutableFile fantomasExecutablePath, FantomasVersion version)
| Error(ProcessStartError.ExecutableFileNotFound _)
| Error(ProcessStartError.UnExpectedException _) -> None)

let findFantomasTool (workingDir: Folder) : Result<FantomasToolFound, FantomasToolError> =
// First try and find a local tool for the folder.
Expand Down Expand Up @@ -217,6 +260,17 @@
ps.Arguments <- "--daemon"
ps
| FantomasToolStartInfo.ToolOnPath(FantomasExecutableFile executableFile) ->
// SECURITY: Validate executable path before starting daemon
if not (validateExecutablePath executableFile) then
return

Check failure on line 265 in src/Fantomas.Client/FantomasToolLocator.fs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

This construct may only be used within computation expressions. To return a value from an ordinary function simply write the expression without 'return'.

Check failure on line 265 in src/Fantomas.Client/FantomasToolLocator.fs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

This construct may only be used within computation expressions. To return a value from an ordinary function simply write the expression without 'return'.

Check failure on line 265 in src/Fantomas.Client/FantomasToolLocator.fs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

This construct may only be used within computation expressions. To return a value from an ordinary function simply write the expression without 'return'.

Check failure on line 265 in src/Fantomas.Client/FantomasToolLocator.fs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

This construct may only be used within computation expressions. To return a value from an ordinary function simply write the expression without 'return'.

Check failure on line 265 in src/Fantomas.Client/FantomasToolLocator.fs

View workflow job for this annotation

GitHub Actions / build (macOS-latest)

This construct may only be used within computation expressions. To return a value from an ordinary function simply write the expression without 'return'.

Check failure on line 265 in src/Fantomas.Client/FantomasToolLocator.fs

View workflow job for this annotation

GitHub Actions / build (macOS-latest)

This construct may only be used within computation expressions. To return a value from an ordinary function simply write the expression without 'return'.
Error(
ProcessStartError.UnExpectedException(
executableFile,
"--daemon",
"Executable path failed security validation. Only known fantomas executables in trusted locations are allowed."
)
)

let ps = ProcessStartInfo(executableFile)
ps.Arguments <- "--daemon"
ps
Expand Down
Loading