A notable threat in application security arises when applications execute commands within directories that may be under an attacker's influence. It's important to note that Windows systems are particularly susceptible to this threat, as the current directory is included in the %PATH%
environment variable. This means that when executing commands, Windows may unintentionally allow executables from the current directory to run, potentially leading to malicious actions if an attacker has placed harmful files there. In contrast, Unix-based systems typically do not include the current directory in the default executable search path, reducing the risk of executing unintended commands.
A prime example of this vulnerability is the case of git-bug, which was impacted by CVE-2021-28955. To address this, the developers implemented a fix using the execabs
library to mitigate the risk of executing commands in insecure environments.
In 2022, with the commit 3ce203db80cd1f320f0c597123b918c3b3bb0449, the Go programming language adopted similar protective measures by default when executing commands. This change is significant as it enhances the security posture of applications built with Go. To illustrate the impact of this change, we can compare the behavior of Go versions 1.18 and 1.19 when running the following code:
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
currentPath := os.Getenv("PATH")
newPath := fmt.Sprintf(".:%s", currentPath)
os.Setenv("PATH", newPath)
cmd := exec.Command("foo")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Command failed with error: %v\n", err)
if exitError, ok := err.(*exec.ExitError); ok {
fmt.Printf("Exit code: %d\n", exitError.ExitCode())
os.Exit(exitError.ExitCode())
} else {
os.Exit(1) // Exit with generic failure if error type is unknown
}
} else {
fmt.Println("Command executed successfully.")
os.Exit(0) // Success exit code
}
}
With Go 1.18, we get the following:
FOO
Command executed successfully.
With Go 1.19, we get the following:
Command failed with error: exec: "foo": cannot run executable found relative to current directory
Despite this improvement, developers occasionally need to run commands in the current directory, leading to potential bypasses of the new security mechanism. For instance, we can see examples of such bypasses in:
ffmpegPath, ffmpegErr = exec.LookPath("ffmpeg")
if errors.Is(ffmpegErr, exec.ErrDot) {
log.Trace("ffmpeg found in current folder '.'")
ffmpegPath, ffmpegErr = exec.LookPath("./ffmpeg")
}
By replacing "ffmpeg"
with "./ffmpeg"
, the code ignores the protection.
cmd := exec.Command(name, args...)
if errors.Is(cmd.Err, exec.ErrDot) {
cmd.Err = nil
}
By setting cmd.Err
to nil
, the code ignores the error.
While these examples demonstrate developers circumventing the protective measures, it's essential to recognize the benefits of the Go team's initiative. The introduction of these protections by default represents a significant improvement in application security.
This development embodies the principle of "MAKE SECURE THE DEFAULT AND INSECURE OBVIOUS." It highlights that while vulnerabilities may still be exploited through workarounds, the standard enforcement of security practices makes it clearer when developers choose to bypass these safeguards.