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

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

このコミットは、Go言語の標準ライブラリにpathパッケージを導入し、ファイルパスを正規化するためのpath.Clean関数およびその他のユーティリティ(Split, Join, Ext)を追加するものです。特に、path.Clean関数はHTTPサーバーにおけるURLのサニタイズに利用され、ディレクトリトラバーサル攻撃を防ぐための重要なセキュリティ強化が図られています。

コミット

commit 16b38b554fb4dae82923cb81a5c6a76ee2959d2f
Author: Russ Cox <rsc@golang.org>
Date:   Tue Apr 7 00:40:07 2009 -0700

    add path.Clean and other utilities.
    
    use path.Clean in web server to sanitize URLs.
    
    http://triv/go/../../../etc/passwd
    
    no longer serves the password file.
    it redirects to
    
    http://triv/etc/passwd
    
    which then gets a 404.
    
    R=r
    DELTA=288  (286 added, 0 deleted, 2 changed)
    OCL=27142
    CL=27152

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

https://github.com/golang/go/commit/16b38b554fb4dae82923cb81a5c6a76ee2959d2f

元コミット内容

path.Cleanおよびその他のユーティリティを追加。 Webサーバーでpath.Cleanを使用してURLをサニタイズ。

例: http://triv/go/../../../etc/passwd のようなURLは、もはやパスワードファイルを提供せず、http://triv/etc/passwd にリダイレクトされ、その後404エラーとなる。

変更の背景

このコミットの主な背景は、Webサーバーにおけるセキュリティ脆弱性、特にディレクトリトラバーサル攻撃への対策です。当時のGo言語のHTTPサーバーは、リクエストされたURLパスに..(親ディレクトリ)のような特殊な要素が含まれている場合、意図しないファイル(例えば、Webサーバーの公開ディレクトリ外にあるシステムファイルなど)にアクセスされる可能性がありました。

コミットメッセージの例にある http://triv/go/../../../etc/passwd は、典型的なディレクトリトラバーサル攻撃の試みを示しています。このURLは、goディレクトリから3階層上のディレクトリ(ルートディレクトリに相当)に移動し、そこから /etc/passwd ファイルにアクセスしようとします。このような攻撃を防ぐためには、URLパスを正規化し、悪意のある要素を排除するメカニズムが必要でした。

path.Clean関数は、このようなパスの正規化を安全に行うための汎用的なツールとして導入され、その機能をHTTPサーバーに組み込むことで、Webアプリケーションのセキュリティを向上させることが目的とされました。

前提知識の解説

ディレクトリトラバーサル (Directory Traversal)

ディレクトリトラバーサル(またはパストラバーサル)は、Webアプリケーションの脆弱性の一種で、攻撃者がファイルシステム上の任意のファイルやディレクトリにアクセスすることを可能にします。これは、ユーザーからの入力(例えばURLパスやファイル名)が適切に検証・サニタイズされずに、ファイルシステム操作に使用される場合に発生します。

攻撃者は、../(親ディレクトリへの移動)のような特殊なシーケンスをパスに挿入することで、アプリケーションが意図したディレクトリの範囲外に「トラバース(移動)」しようとします。これにより、設定ファイル、ソースコード、パスワードファイルなど、通常は公開されるべきではない機密情報にアクセスしたり、場合によってはファイルを書き換えたりする可能性があります。

path.Clean の正規化ルール

path.Clean関数は、パスを「純粋に字句解析によって」最短の等価なパス名に変換します。これは以下のルールを繰り返し適用することで行われます。

  1. 複数のスラッシュを単一のスラッシュに置換: 例: /a//b/a/b になります。
  2. . (カレントディレクトリ) 要素の排除: 例: /a/./b/a/b になります。
  3. .. (親ディレクトリ) 要素と、その直前の非..要素の排除: 例: /a/b/../c/a/c になります。
  4. ルートパスの先頭にある .. 要素の排除: 例: /../a/a になります。

これらのルールにより、path.Clean../../../ のような悪意のあるシーケンスを効果的に除去し、パスを正規化された形式に変換します。結果が空文字列になる場合は、.(カレントディレクトリ)を返します。

Go言語のHTTPサーバー (net/http パッケージの初期段階)

このコミットが行われた2009年当時、Go言語のnet/httpパッケージはまだ初期段階にありました。http.ServeMuxは、リクエストURLのパスに基づいて適切なハンドラにディスパッチする役割を担っていました。このコミットでは、ServeMuxがリクエストパスを処理する前にpath.Cleanを適用することで、セキュリティを強化しています。

技術的詳細

このコミットは、主に以下の3つの側面で技術的な変更を加えています。

  1. pathパッケージの新規追加: src/lib/path.goとして新しいパッケージが追加され、Clean, Split, Join, Extといったパス操作ユーティリティが提供されます。

    • Clean(path string) string: パスを正規化し、最短の等価なパス名を返します。
    • Split(path string) (dir, file string): パスをディレクトリ部分とファイル名部分に分割します。
    • Join(dir, file string) string: ディレクトリとファイル名を結合してパスを生成します。
    • Ext(path string) string: ファイル名の拡張子を返します。 これらの関数は、ファイルパスの操作において非常に基本的ながらも重要な機能を提供します。
  2. HTTPサーバーへのpath.Cleanの統合: src/lib/http/server.goが変更され、http.ServeMuxServeHTTPメソッド内でpath.Cleanが利用されるようになります。

    • cleanPathというヘルパー関数が導入され、リクエストURLのパスをpath.Cleanで処理し、必要に応じて末尾のスラッシュを保持するロジックが追加されています。
    • ServeHTTPメソッドの冒頭で、リクエストパスがcleanPathによって正規化されたパスと異なる場合、StatusMovedPermanently (301) リダイレクトを返すことで、正規化されたURLへのアクセスを強制します。これにより、悪意のあるパスが直接処理されることを防ぎます。
  3. テストの追加: src/lib/path_test.goが新規追加され、path.Clean, Splitなどの関数が期待通りに動作するかを検証する単体テストが記述されています。特にClean関数には、様々なエッジケース(空文字列、., .., 複数のスラッシュ、末尾のスラッシュなど)を網羅したテストケースが用意されており、堅牢性が確保されています。

この変更により、Go言語のHTTPサーバーは、ユーザーが提供するURLパスの安全性を自動的に高めることができるようになりました。

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

src/lib/http/server.go

 // ServeHTTP dispatches the request to the handler whose
 // pattern most closely matches the request URL.
 func (mux *ServeMux) ServeHTTP(c *Conn, req *Request) {
+	// Clean path to canonical form and redirect.
+	if p := cleanPath(req.Url.Path); p != req.Url.Path {
+		c.SetHeader("Location", p);
+		c.WriteHeader(StatusMovedPermanently);
+		return;
+	}
+
 	// Most-specific (longest) pattern wins.
 	var h Handler;
 	var n = 0;

上記はServeMuxServeHTTPメソッドの変更点です。リクエストパスをcleanPath関数で正規化し、元のパスと異なる場合は正規化されたパスへ301リダイレクトを行います。

また、cleanPath関数が新しく追加されています。

// Return the canonical path for p, eliminating . and .. elements.
func cleanPath(p string) string {
	if p == "" {
		return "/";
	}
	if p[0] != '/' {
		p = "/" + p;
	}
	np := path.Clean(p);
	// path.Clean removes trailing slash except for root;
	// put the trailing slash back if necessary.
	if p[len(p)-1] == '/' && np != "/" {
		np += "/";
	}
	return np;
}

このcleanPath関数は、入力パスが空の場合に/を返し、ルートからのパスでない場合は先頭に/を追加します。そして、path.Cleanを呼び出してパスを正規化します。path.Cleanはルートパス以外では末尾のスラッシュを削除するため、元のパスに末尾のスラッシュがあった場合は、正規化後もそれを保持するように調整しています。

src/lib/path.go (新規追加ファイル)

このファイルには、Clean, Split, Join, Extの各関数の実装が含まれています。特にClean関数の実装は、字句解析によるパスの正規化ロジックが詳細に記述されています。

func Clean(path string) string {
	if path == "" {
		return "."
	}

	rooted := path[0] == '/'
	n := len(path)

	// ... (詳細な字句解析ロジック) ...

	return string(buf[0:w])
}

Clean関数の内部では、パスをバイトスライスとして扱い、r(読み込みインデックス)、w(書き込みインデックス)、dotdot..要素が停止すべき位置)を管理しながら、前述の正規化ルールを適用しています。

src/lib/path_test.go (新規追加ファイル)

このファイルには、pathパッケージの各関数の単体テストが含まれています。

type CleanTest struct {
	path, clean string
}

var cleantests = []CleanTest {
	// Already clean
	CleanTest{"", "."},
	CleanTest{"abc", "abc"},
	// ... (その他のテストケース) ...
}

func TestClean(t *testing.T) {
	for i, test := range cleantests {
		if s := Clean(test.path); s != test.clean {
			t.Errorf("Clean(%q) = %q, want %q", test.path, s, test.clean);
		}
	}
}

CleanTest構造体とcleantestsスライスを使って、様々な入力パスとその期待される正規化結果を定義し、TestClean関数でそれらを検証しています。同様にTestSplit, TestJoin, TestExtも定義されています。

コアとなるコードの解説

このコミットの核心は、path.Clean関数が提供するパスの字句正規化機能と、それがHTTPサーバーに統合された方法にあります。

path.Cleanは、ファイルシステムパスのセマンティクスを理解することなく、純粋に文字列操作によってパスを簡潔な形式に変換します。これにより、../のような特殊なシーケンスが悪用されるのを防ぎます。例えば、http://triv/go/../../../etc/passwdというリクエストパスが来た場合、path.Cleanはこれを/etc/passwdに正規化します。

HTTPサーバーのServeMux.ServeHTTPメソッドでは、この正規化されたパスと元のリクエストパスを比較します。もし両者が異なる場合、サーバーは正規化されたパスへのHTTP 301リダイレクトをクライアントに返します。これにより、クライアントは正規化された安全なURLで再度リクエストを行うことになり、サーバーは常にクリーンなパスで処理を行うことができます。このリダイレクトの仕組みは、悪意のあるパスがサーバー内部で直接処理されることを防ぐための重要な防御策となります。

このアプローチは、ディレクトリトラバーサル攻撃に対する第一線の防御として機能します。ただし、Web検索の結果にもあるように、path.Cleanはあくまで字句解析によるサニタイズであり、それ単独で全てのパス関連の脆弱性を防ぐわけではありません。例えば、正規化されたパスが依然として意図しないファイルシステム上の場所を指す可能性は残ります(例: path.Cleanはシンボリックリンクを解決しない)。しかし、このコミットの時点では、基本的なディレクトリトラバーサル攻撃を防ぐための非常に効果的なステップでした。

関連リンク

参考にした情報源リンク