LoginSignup
0
0

More than 1 year has passed since last update.

Go 1.19 で os/exec パッケージの挙動が変わった話【転載版】

Posted at

Go強化月間と聞いたので(笑) この記事は自ブログからの転載です。書き捨てなのでメンテナンスしません。悪しからずご了承の程を)

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

Command and LookPath no longer allow results from a PATH search to be found relative to the current directory. This removes a common source of security problems but may also break existing programs that depend on using, say, exec.Command("prog") to run a binary named prog (or, on Windows, prog.exe) in the current directory. See the os/exec package documentation for information about how best to update such programs.
(via “Go 1.19 Release Notes)

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

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

sample.go
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.ErrDot エラーを明示的に潰すことで実現できる。こんな感じ。

sample2.go
package main

import (
    "errors"
    "fmt"
    "os/exec"
)

func main() {
    cmd := exec.Command("gpgpdump.exe", "version")
    if cmd.Err != nil {
        fmt.Println(cmd.Err)
        if !errors.Is(cmd.Err, exec.ErrDot) {
            return
        }
        cmd.Err = nil
    }
    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 ごとに処理を分ける必要がなくなったわけだ。めでたい!

ところで Windows には NoDefaultCurrentDirectoryInExePath なる環境変数があるそうで,これが有効になっているとパスなしのコマンド指定時にカレントフォルダの同名実行ファイルを無視するらしい。で, os/exec パッケージは律儀にこの環境変数にも対応している。

On Windows, Command and LookPath now respect the NoDefaultCurrentDirectoryInExePath environment variable, making it possible to disable the default implicit search of “.” in PATH lookups on Windows systems.
(via “Go 1.19 Release Notes)

少々姑息ではあるが,これを利用してカレントフォルダの同名実行ファイルを無視するよう構成することもできる。こんな感じ。

sample3.go
package main

import (
    "errors"
    "fmt"
    "os"
    "os/exec"
)

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

これを実行すると

> go run sample3.go
exec: "gpgpdump.exe": executable file not found in %PATH%

となる。前のコードの実行結果で出力されるエラーメッセージの違いを確かめてほしい。Windows 以外でこの環境変数が悪さをすることはないだろうから Linux 等と挙動を合わせたいなら,おまじない的にセットしておくのもいいかもしれない。

やっぱ Windows は面倒くさいな(笑)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0