https://text.baldanders.info/golang/exec-package-in-go119/

Go 1.19 のリリースノートを眺めてみると

via Go 1.19 Release Notes - The Go Programming Language

とある。 さっそく試してみよう。

まず Windows 環境で gpgpdump.exe コマンドを PATH で指定されたフォルダ以外,具体的には以下のソースファイルと同じフォルダに置く。

package main
import (
    "fmt"
    "os/exec"
)
func main() {
    cmd := "gpgpdump.exe"
    out, err := exec.Command(cmd, "version").CombinedOutput()
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("output by %v:\\n%v\\n", cmd, string(out))
}

これを Go 1.19 コンパイル環境下で実行すると

> go run sample.go
exec: "gpgpdump.exe": cannot run executable found relative to current directory

「カレントディレクトリに指定の実行ファイルあるけど起動しちゃらん(←超意訳,出雲弁)」とエラーになった。

Windows ではパス指定なしでコマンドを起動する際に,カレントフォルダに同名の実行ファイルが存在すると優先的にそれを起動してしまう。 Go 標準の os/exec パッケージもこの挙動に合わせていたのだが,2020年の CVE-2020-27955 で問題になった。 この挙動を悪用して悪意のコマンドを実行される可能性があるというわけだ。

この脆弱性を回避するために,様々な試行錯誤が行われたが Go 1.19 の改修が決定打になるだろう。 カレントフォルダにある同名の実行ファイルを無視するのではなく,エラーとして「起動させない」というのがポイント。

なお,今まで通りパスなしのコマンド指定時にカレントフォルダの実行ファイルを起動したいなら [exec](<https://pkg.go.dev/os/exec>).ErrDot エラーを明示的に潰すことで実現できる。 こんな感じ。

package main
import (
    "errors"
    "fmt"
    "os/exec"
)
func main() {
    cmd := exec.Command("gpgpdump.exe", "version")
    out, err := cmd.CombinedOutput()
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("output by %v:\\n%v\\n", cmd, string(out))
}

これを実行すると

> go run sample2.go
exec: "gpgpdump.exe": cannot run executable found relative to current directory
output by .\\gpgpdump.exe version:
gpgpdump v0.14.0
repository: <https://github.com/goark/gpgpdump>

となる。 エラーを無視してカレントディレクトリ . を付加した状態で実行されているのがお分かりだろうか。

ちなみに,同じコードを Windows 以外の環境で実行すると(.exe の拡張子は外してね)

$ go run sample2b.go
exec: "gpgpdump": executable file not found in $PATH

と PATH 上に実行ファイルが見つからない旨の普通のエラーが表示される。 これでアプリケーション側は OS ごとに処理を分ける必要がなくなったわけだ。 めでたい!