Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 15157] ファイルの概要

このコミットは、Go言語の標準ライブラリos/execパッケージ内のLookPath関数の挙動に関する修正です。具体的には、Unix系システムにおいてPATH環境変数が空の場合に、LookPathが誤ってカレントワーキングディレクトリ (cwd) を検索してしまう問題を解決します。この変更により、セキュリティと予測可能性が向上します。

コミット

commit fe7dbea00e36d649f072d38070630c53c4efab17
Author: Péter Surányi <speter.go1@gmail.com>
Date:   Thu Feb 7 06:41:35 2013 -0800

    os/exec: LookPath on Unix shouldn't look in cwd when PATH is empty

    R=golang-dev, dave, bradfitz
    CC=golang-dev
    https://golang.org/cl/7305053

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/fe7dbea00e36d649f072d38070630c53c4efab17

元コミット内容

os/exec: LookPath on Unix shouldn't look in cwd when PATH is empty

このコミットは、Unix系システムにおけるLookPath関数の動作を修正します。PATH環境変数が空の場合、LookPathはカレントワーキングディレクトリ (cwd) を検索すべきではありません。

変更の背景

os/execパッケージのLookPath関数は、指定された実行可能ファイルがシステム上のどこにあるかをPATH環境変数に基づいて検索する役割を担っています。Unix系システムでは、伝統的にPATH環境変数が空の場合、シェルはカレントワーキングディレクトリ (cwd) を検索パスに含めません。これは、悪意のある実行ファイルがカレントディレクトリに置かれている場合に、意図しないプログラムが実行されるのを防ぐためのセキュリティ上の慣習です。

しかし、GoのLookPath関数は、PATH環境変数が空であっても、内部的にカレントワーキングディレクトリを検索パスの一部として扱ってしまうバグがありました。この挙動は、Unixシェルの標準的な動作と異なり、予期せぬプログラムの実行やセキュリティ上の脆弱性につながる可能性がありました。例えば、ユーザーがPATHを意図的に空に設定していても、LookPathを使用するプログラムがカレントディレクトリにある同名のスクリプトや実行ファイルを誤って実行してしまうリスクがありました。

このコミットは、この不整合を解消し、LookPathがUnixシェルの標準的なセキュリティプラクティスに準拠するように修正することを目的としています。

前提知識の解説

os/execパッケージ

Go言語の標準ライブラリであるos/execパッケージは、外部コマンドを実行するための機能を提供します。このパッケージを使用することで、Goプログラムからシェルコマンドや他の実行可能ファイルを起動し、その入出力を制御することができます。

LookPath関数

os/execパッケージ内のLookPath関数は、与えられたファイル名(実行可能ファイル名)がPATH環境変数で指定されたディレクトリ群のどこに存在するかを検索し、その絶対パスを返します。例えば、LookPath("ls")と呼び出すと、システム上のlsコマンドの絶対パス(例: /bin/ls)を返します。

PATH環境変数

PATH環境変数は、オペレーティングシステムが実行可能ファイルを検索するディレクトリのリストを定義する環境変数です。Unix系システムでは、コロン (:) で区切られたディレクトリパスのリストとして表現されます。例えば、PATH=/usr/local/bin:/usr/bin:/binのような値が設定されている場合、システムはまず/usr/local/bin、次に/usr/bin、最後に/binの順で実行可能ファイルを検索します。

カレントワーキングディレクトリ (cwd)

カレントワーキングディレクトリ (Current Working Directory, cwd) は、現在作業しているディレクトリを指します。シェルやプログラムがファイルや他のプログラムを検索する際に、相対パスが指定された場合の基準となるディレクトリです。Unix系システムでは、PATH環境変数に明示的に.(カレントディレクトリ)が含まれていない限り、通常はcwdは実行可能ファイルの検索パスには含まれません。これは、セキュリティ上の理由から推奨される慣行です。

技術的詳細

このコミットの技術的な核心は、LookPath関数がPATH環境変数の値を評価するロジックにあります。修正前は、os.Getenv("PATH")PATH環境変数の値を取得した後、その値が空文字列 ("") であっても、後続のループ処理でパス要素として空文字列 ("") が現れた場合に、それをカレントワーキングディレクトリ (.) として解釈していました。

Unixシェルのセマンティクスでは、PATH内の空の要素はカレントディレクトリを意味しますが、これはPATH自体が空でない場合に適用されるルールです。PATH全体が空である場合は、実行可能ファイルの検索は行われないか、あるいは特定のシステムディレクトリのみに限定されるべきです。

このコミットでは、os.Getenv("PATH")で取得したpathenvが空文字列である場合に、即座にErrNotFoundを返して処理を終了するチェックを追加しています。これにより、PATHが空の場合に誤ってカレントワーキングディレクトリを検索してしまうという、Unixシェルの慣習に反する挙動が修正されます。

また、この変更を検証するために、新しいテストケースTestLookPathUnixEmptyPathが追加されました。このテストは、一時ディレクトリを作成し、そのディレクトリに移動した後、PATH環境変数を一時的に空に設定します。そして、そのディレクトリ内に実行可能ファイルを作成し、LookPathがそのファイルを見つけないことを確認します。これにより、修正が正しく機能していることが保証されます。

コアとなるコードの変更箇所

変更は以下の2つのファイルで行われています。

  1. src/pkg/os/exec/lp_unix.go: LookPath関数の実装ファイル
  2. src/pkg/os/exec/lp_unix_test.go: LookPath関数のテストファイル(新規追加)

src/pkg/os/exec/lp_unix.go の変更

--- a/src/pkg/os/exec/lp_unix.go
+++ b/src/pkg/os/exec/lp_unix.go
@@ -42,6 +42,9 @@ func LookPath(file string) (string, error) {
 		return "", &Error{file, err}
 	}
 	pathenv := os.Getenv("PATH")
+	if pathenv == "" {
+		return "", &Error{file, ErrNotFound}
+	}
 	for _, dir := range strings.Split(pathenv, ":") {
 		if dir == "" {
 			// Unix shell semantics: path element "" means "."

src/pkg/os/exec/lp_unix_test.go の新規追加

--- /dev/null
+++ b/src/pkg/os/exec/lp_unix_test.go
@@ -0,0 +1,52 @@
+// Copyright 2013 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin freebsd linux netbsd openbsd
+
+package exec
+
+import (
+	"io/ioutil"
+	"os"
+	"testing"
+)
+
+func TestLookPathUnixEmptyPath(t *testing.T) {
+	tmp, err := ioutil.TempDir("", "TestLookPathUnixEmptyPath")
+	if err != nil {
+		t.Fatal("TempDir failed: ", err)
+	}
+	defer os.RemoveAll(tmp)
+	wd, err := os.Getwd()
+	if err != nil {
+		t.Fatal("Getwd failed: ", err)
+	}
+	err = os.Chdir(tmp)
+	if err != nil {
+		t.Fatal("Chdir failed: ", err)
+	}
+	defer os.Chdir(wd)
+
+	f, err := os.OpenFile("exec_me", os.O_CREATE|os.O_EXCL, 0700)
+	if err != nil {
+		t.Fatal("OpenFile failed: ", err)
+	}
+	defer f.Close()
+
+	pathenv := os.Getenv("PATH")
+	defer os.Setenv("PATH", pathenv)
+
+	err = os.Setenv("PATH", "")
+	if err != nil {
+		t.Fatal("Setenv failed: ", err)
+	}
+
+	path, err := LookPath("exec_me")
+	if err == nil {
+		t.Fatal("LookPath found exec_me in empty $PATH")
+	}
+	if path != "" {
+		t.Fatalf("LookPath path == %q when err != nil", path)
+	}
+}

コアとなるコードの解説

src/pkg/os/exec/lp_unix.go の変更点

追加された3行のコードは、LookPath関数の冒頭近くに挿入されています。

	pathenv := os.Getenv("PATH")
	if pathenv == "" {
		return "", &Error{file, ErrNotFound}
	}
  1. pathenv := os.Getenv("PATH"): まず、現在のPATH環境変数の値を取得し、pathenv変数に格納します。
  2. if pathenv == "": 取得したpathenvが空文字列であるかどうかをチェックします。
  3. return "", &Error{file, ErrNotFound}: もしpathenvが空であれば、指定されたファイルが見つからなかったことを示すエラーErrNotFoundを返して、関数の実行を即座に終了します。

このシンプルなチェックにより、PATHが空の場合に後続のstrings.Split(pathenv, ":")が空のスライスを返し、その結果としてカレントディレクトリが検索されてしまうという誤った挙動が防止されます。これにより、Unixシェルの標準的なセキュリティ慣行に準拠した動作が実現されます。

src/pkg/os/exec/lp_unix_test.go の新規追加

このテストファイルは、+buildディレクティブによりUnix系システム(darwin, freebsd, linux, netbsd, openbsd)でのみビルドされるように設定されています。

TestLookPathUnixEmptyPath関数は以下の手順でテストを実行します。

  1. 一時ディレクトリの作成と移動:
    • ioutil.TempDirで一時ディレクトリを作成し、テスト終了時にdefer os.RemoveAll(tmp)でクリーンアップします。
    • os.Getwd()で現在のワーキングディレクトリを保存し、os.Chdir(tmp)で一時ディレクトリに移動します。テスト終了時にdefer os.Chdir(wd)で元のディレクトリに戻ります。これにより、テストが他のテストやシステムの状態に影響を与えないようにします。
  2. テスト用実行可能ファイルの作成:
    • os.OpenFile("exec_me", os.O_CREATE|os.O_EXCL, 0700)で、一時ディレクトリ内にexec_meという名前の実行可能ファイルを作成します。パーミッション0700は、所有者のみが読み書き実行できることを意味します。
  3. PATH環境変数の操作:
    • pathenv := os.Getenv("PATH")で現在のPATH環境変数の値を保存し、defer os.Setenv("PATH", pathenv)でテスト終了時に元の値に戻します。
    • os.Setenv("PATH", "")PATH環境変数を意図的に空に設定します。これがテストの肝となる部分です。
  4. LookPathの呼び出しと検証:
    • path, err := LookPath("exec_me")で、PATHが空の状態でexec_meを検索します。
    • if err == nil { t.Fatal("LookPath found exec_me in empty $PATH") }: PATHが空であるため、LookPathexec_meを見つけるべきではありません。もしエラーがnil(つまり見つかった)であれば、テストは失敗します。
    • if path != "" { t.Fatalf("LookPath path == %q when err != nil", path) }: エラーが発生した場合、返されるパスは空文字列であるべきです。もし空でなければ、テストは失敗します。

このテストは、PATHが空の場合にLookPathがカレントディレクトリを検索しないという、修正後の正しい挙動を厳密に検証しています。

関連リンク

参考にした情報源リンク