Package execabs is a drop-in replacement for os/exec that requires PATH lookups to find absolute paths.
golang.org/x/sys/execabsPackage execabs provides a more secure alternative to os/exec by preventing the execution of programs from the current directory through PATH lookups. When a relative path is returned from a PATH search, execabs will return an error instead of executing the program.
This addresses a security vulnerability where malicious executables in the current directory could be executed unintentionally. See https://blog.golang.org/path-security for more information about when it may be necessary or appropriate to use this package.
import "golang.org/x/sys/execabs"The execabs package differs from os/exec in the following ways:
PATH Lookups: If exec.Command would have returned an exec.Cmd configured to run an executable from the current directory, execabs.Command instead returns an exec.Cmd that will return an error from Start or Run.
LookPath Behavior: If exec.LookPath's PATH lookup would have returned an executable from the current directory, execabs.LookPath instead returns an error.
Absolute Paths Required: The result of PATH lookups must be absolute paths.
var ErrNotFound = exec.ErrNotFoundErrNotFound is the error resulting if a path search failed to find an executable file. It is an alias for exec.ErrNotFound.
func Command(name string, arg ...string) *exec.CmdCommand returns the Cmd struct to execute the named program with the given arguments. See exec.Command for most details.
Command differs from exec.Command in its handling of PATH lookups, which are used when the program name contains no slashes. If exec.Command would have returned an exec.Cmd configured to run an executable from the current directory, Command instead returns an exec.Cmd that will return an error from Start or Run.
Parameters:
name: The name of the program to executearg: Variable number of command-line argumentsReturns:
*exec.Cmd: A Cmd structure that can be used to execute the commandfunc CommandContext(ctx context.Context, name string, arg ...string) *exec.CmdCommandContext is like Command but includes a context.
The provided context is used to kill the process (by calling os.Process.Kill) if the context becomes done before the command completes on its own.
Parameters:
ctx: Context for cancellation and timeout controlname: The name of the program to executearg: Variable number of command-line argumentsReturns:
*exec.Cmd: A Cmd structure that can be used to execute the commandfunc LookPath(file string) (string, error)LookPath searches for an executable named file in the directories named by the PATH environment variable. If file contains a slash, it is tried directly and the PATH is not consulted. The result will be an absolute path.
LookPath differs from exec.LookPath in its handling of PATH lookups, which are used for file names without slashes. If exec.LookPath's PATH lookup would have returned an executable from the current directory, LookPath instead returns an error.
Parameters:
file: The name of the executable to search forReturns:
string: The absolute path to the executableerror: An error if the executable was not found or if it would have resolved to the current directorytype Cmd = exec.CmdCmd represents an external command being prepared or run. It is an alias for exec.Cmd.
type Error = exec.ErrorError is returned by LookPath when it fails to classify a file as an executable. It is an alias for exec.Error.
type ExitError = exec.ExitErrorAn ExitError reports an unsuccessful exit by a command. It is an alias for exec.ExitError.
package main
import (
"fmt"
"log"
"golang.org/x/sys/execabs"
)
func main() {
// Execute a command safely - will not run executables from current directory
cmd := execabs.Command("ls", "-la")
output, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(output))
}package main
import (
"context"
"fmt"
"log"
"time"
"golang.org/x/sys/execabs"
)
func main() {
// Create a context with 5 second timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Execute command with timeout
cmd := execabs.CommandContext(ctx, "sleep", "10")
err := cmd.Run()
if err != nil {
// Will error with "signal: killed" after 5 seconds
log.Printf("Command failed: %v", err)
}
}package main
import (
"fmt"
"log"
"golang.org/x/sys/execabs"
)
func main() {
// Look up the absolute path to an executable
path, err := execabs.LookPath("git")
if err != nil {
log.Fatal(err)
}
fmt.Printf("git found at: %s\n", path)
// Output: git found at: /usr/bin/git
// This will fail if there's a "git" executable in current directory
// but not in PATH
}package main
import (
"bytes"
"fmt"
"log"
"golang.org/x/sys/execabs"
)
func main() {
cmd := execabs.Command("git", "status")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
fmt.Printf("Error: %v\n", err)
fmt.Printf("Stderr: %s\n", stderr.String())
return
}
fmt.Printf("Output:\n%s\n", stdout.String())
}package main
import (
"fmt"
"golang.org/x/sys/execabs"
)
func isCommandAvailable(name string) bool {
_, err := execabs.LookPath(name)
return err == nil
}
func main() {
if isCommandAvailable("docker") {
fmt.Println("Docker is installed")
} else {
fmt.Println("Docker is not available")
}
if isCommandAvailable("kubectl") {
fmt.Println("kubectl is installed")
} else {
fmt.Println("kubectl is not available")
}
}package main
import (
"fmt"
"log"
"golang.org/x/sys/execabs"
)
func main() {
// First command: generate data
cmd1 := execabs.Command("echo", "hello world")
// Second command: convert to uppercase
cmd2 := execabs.Command("tr", "a-z", "A-Z")
// Connect cmd1's stdout to cmd2's stdin
cmd2.Stdin, _ = cmd1.StdoutPipe()
// Start both commands
cmd2.Start()
cmd1.Run()
// Get output from cmd2
output, err := cmd2.Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(output))
// Output: HELLO WORLD
}package main
import (
"fmt"
"log"
"os"
"golang.org/x/sys/execabs"
)
func main() {
cmd := execabs.Command("pwd")
// Set working directory
cmd.Dir = "/tmp"
// Set environment variables
cmd.Env = append(os.Environ(),
"CUSTOM_VAR=value",
"ANOTHER_VAR=another_value",
)
output, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Working directory: %s", string(output))
}package main
import (
"fmt"
"os/exec"
"golang.org/x/sys/execabs"
)
func main() {
cmd := execabs.Command("false") // 'false' command always exits with code 1
err := cmd.Run()
if err != nil {
// Check if it's an ExitError to get the exit code
if exitErr, ok := err.(*exec.ExitError); ok {
fmt.Printf("Command exited with code: %d\n", exitErr.ExitCode())
} else {
fmt.Printf("Command failed: %v\n", err)
}
}
}package main
import (
"bufio"
"fmt"
"log"
"golang.org/x/sys/execabs"
)
func main() {
cmd := execabs.Command("ping", "-c", "5", "golang.org")
// Get a pipe to read from stdout
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
}
// Start the command
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
// Read output line by line in real-time
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
// Wait for command to complete
if err := cmd.Wait(); err != nil {
log.Fatal(err)
}
}The execabs package addresses a security vulnerability in the standard os/exec package. Consider this scenario:
// Using os/exec (POTENTIALLY UNSAFE)
import "os/exec"
cmd := exec.Command("git", "status")
cmd.Run()If there's a malicious executable named git in the current directory, and the current directory is in your PATH (or appears before the real git in PATH), the malicious version will be executed instead.
// Using execabs (SAFE)
import "golang.org/x/sys/execabs"
cmd := execabs.Command("git", "status")
cmd.Run()With execabs, even if there's a git executable in the current directory, it will not be executed. Instead, only executables found via absolute paths from PATH lookups are allowed.
Use execabs when:
Migration is straightforward - just change the import:
// Before
import "os/exec"
// After
import "golang.org/x/sys/execabs"The API is identical, so no code changes are needed beyond the import statement.
Prefer execabs over os/exec - Unless you have a specific need for the os/exec behavior, use execabs for better security.
Use absolute paths when possible - If you know the exact path to an executable, specify it directly:
cmd := execabs.Command("/usr/bin/git", "status")Validate command names - If accepting command names from external sources, validate them against an allowlist.
Use CommandContext for timeouts - Always use context for long-running commands to prevent hanging:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := execabs.CommandContext(ctx, "slow-command")Check errors from LookPath - Before executing a command, use LookPath to verify it exists:
if _, err := execabs.LookPath("docker"); err != nil {
return fmt.Errorf("docker not found: %w", err)
}Handle exit codes properly - Check for ExitError to distinguish between different failure modes:
if err := cmd.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
// Command ran but exited with non-zero status
fmt.Printf("Exit code: %d\n", exitErr.ExitCode())
} else {
// Command failed to start or other error
fmt.Printf("Failed to run: %v\n", err)
}
}os/exec - Standard library command execution (less secure)context - Context for cancellation and timeoutsgolang.org/x/sys/unix - Low-level Unix system callsgolang.org/x/sys/windows - Low-level Windows system calls