[インデックス 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
関数は、パスを「純粋に字句解析によって」最短の等価なパス名に変換します。これは以下のルールを繰り返し適用することで行われます。
- 複数のスラッシュを単一のスラッシュに置換: 例:
/a//b
は/a/b
になります。 .
(カレントディレクトリ) 要素の排除: 例:/a/./b
は/a/b
になります。..
(親ディレクトリ) 要素と、その直前の非..
要素の排除: 例:/a/b/../c
は/a/c
になります。- ルートパスの先頭にある
..
要素の排除: 例:/../a
は/a
になります。
これらのルールにより、path.Clean
は ../../../
のような悪意のあるシーケンスを効果的に除去し、パスを正規化された形式に変換します。結果が空文字列になる場合は、.
(カレントディレクトリ)を返します。
Go言語のHTTPサーバー (net/http
パッケージの初期段階)
このコミットが行われた2009年当時、Go言語のnet/http
パッケージはまだ初期段階にありました。http.ServeMux
は、リクエストURLのパスに基づいて適切なハンドラにディスパッチする役割を担っていました。このコミットでは、ServeMux
がリクエストパスを処理する前にpath.Clean
を適用することで、セキュリティを強化しています。
技術的詳細
このコミットは、主に以下の3つの側面で技術的な変更を加えています。
-
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
: ファイル名の拡張子を返します。 これらの関数は、ファイルパスの操作において非常に基本的ながらも重要な機能を提供します。
-
HTTPサーバーへの
path.Clean
の統合:src/lib/http/server.go
が変更され、http.ServeMux
のServeHTTP
メソッド内でpath.Clean
が利用されるようになります。cleanPath
というヘルパー関数が導入され、リクエストURLのパスをpath.Clean
で処理し、必要に応じて末尾のスラッシュを保持するロジックが追加されています。ServeHTTP
メソッドの冒頭で、リクエストパスがcleanPath
によって正規化されたパスと異なる場合、StatusMovedPermanently
(301) リダイレクトを返すことで、正規化されたURLへのアクセスを強制します。これにより、悪意のあるパスが直接処理されることを防ぎます。
-
テストの追加:
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;
上記はServeMux
のServeHTTP
メソッドの変更点です。リクエストパスを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
はシンボリックリンクを解決しない)。しかし、このコミットの時点では、基本的なディレクトリトラバーサル攻撃を防ぐための非常に効果的なステップでした。
関連リンク
- Go
path
パッケージのドキュメント (現在のバージョン): https://pkg.go.dev/path - Go
net/http
パッケージのドキュメント (現在のバージョン): https://pkg.go.dev/net/http - Rob Pike, ``Lexical File Names in Plan 9 or Getting Dot-Dot right,'' (コミットメッセージで参照されている論文): http://plan9.bell-labs.com/sys/doc/lexnames.html
参考にした情報源リンク
- Go言語の
path.Clean
に関するWeb検索結果 (ディレクトリトラバーサル、セキュリティ関連情報)- https://labex.io/
- https://stackhawk.com/
- https://rowin.dev/
- https://www.google.com/ (CVE-2022-41722 関連情報)
- https://github.com/ (CVE-2022-41722 関連情報)
- https://cvedetails.com/ (CVE-2022-41722 関連情報)
- https://snyk.io/