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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおけるセキュリティ脆弱性とその修正に関するものです。具体的には、FileServer が生成するディレクトリインデックスにおいて、ファイル名に含まれる特殊文字が適切にエスケープされていなかった問題に対処しています。これにより、悪意のあるファイル名がHTMLインジェクションやURLの破損を引き起こす可能性がありました。

コミット

commit 26cc10289f6e0dd2cebf0195f1351d6790ed7a9e
Author: Michael Kelly <mjk@google.com>
Date:   Tue Jan 14 12:55:12 2014 -0800

    net/http: escape contents of the directory indexes generated by FileServer
    
          Previously, filenames containing special characters could:
          1) Escape the <a> tag, with a file called something like: ">foo
          2) Break the links in the index by prematurely ending the path portion
          of the url, with a file called: foo?bar
    
          In order to avoid a forbidden dependency on the html package, I'm
          using htmlReplacer from net/http/server.go, which is equivalent to
          html.EscapeString.
    
          This change also expands fakeFile.Readdir to better emulate
    os.File.Readdir.
    
    R=golang-codereviews, rsc, gobot, bradfitz, josharian, mikioh.mikioh
    CC=golang-codereviews
    https://golang.org/cl/37440043

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

https://github.com/golang/go/commit/26cc10289f6e0dd2cebf01951d6790ed7a9e

元コミット内容

net/http: escape contents of the directory indexes generated by FileServer

以前は、特殊文字を含むファイル名が以下の問題を引き起こす可能性がありました。

  1. <a> タグをエスケープする(例: ">foo というファイル名)
  2. URLのパス部分を途中で終了させることで、インデックス内のリンクを破壊する(例: foo?bar というファイル名)

html パッケージへの不必要な依存を避けるため、net/http/server.gohtmlReplacer を使用しています。これは html.EscapeString と同等です。

この変更は、fakeFile.Readdiros.File.Readdir をより良くエミュレートするように拡張しています。

変更の背景

このコミットの背景には、Webアプリケーションにおける一般的なセキュリティ脆弱性であるHTMLインジェクション(またはクロスサイトスクリプティング - XSS)の防止があります。net/http パッケージの FileServer は、ディレクトリの内容を一覧表示する際に、ファイル名をHTMLとして出力します。このとき、ファイル名にHTMLの特殊文字(<, >, &, ", ' など)やURLの特殊文字(?, # など)が含まれていると、以下のような問題が発生する可能性がありました。

  1. HTMLインジェクション: 悪意のあるユーザーが、例えばファイル名を "><script>alert('XSS')</script> のように作成した場合、FileServer がこれをエスケープせずにHTMLに出力すると、ブラウザはそのスクリプトを実行してしまいます。これにより、セッションハイジャック、情報の盗難、サイトの改ざんなど、様々な攻撃が可能になります。
  2. URLの破損: ファイル名に ?# といったURLのクエリパラメータやフラグメント識別子として解釈される文字が含まれている場合、FileServer が生成するリンクが意図しないURLになってしまい、リンクが機能しなくなるか、誤ったリソースを指す可能性がありました。

このコミットは、これらの脆弱性を解消し、FileServer がより安全にディレクトリインデックスを提供できるようにするために導入されました。

前提知識の解説

HTMLエスケープ

HTMLエスケープとは、HTMLドキュメント内で特殊な意味を持つ文字(例: < はタグの開始、> はタグの終了、& は実体参照の開始)を、その文字自体として表示させるために、別の表現(実体参照)に変換する処理のことです。例えば、<&lt; に、>&gt; に、&&amp; に、"&quot; に、'&#39; または &apos; に変換されます。これにより、ブラウザがこれらの文字をHTMLタグや実体参照として解釈するのを防ぎ、意図しないHTMLのレンダリングやスクリプトの実行を防ぎます。

URLエンコーディング

URLエンコーディング(パーセントエンコーディングとも呼ばれる)は、URL内で特殊な意味を持つ文字(例: / はパスの区切り、? はクエリパラメータの開始、# はフラグメントの開始、 (スペース)は空白)を、安全にURLに含めるために、% の後にその文字のASCII値またはUTF-8バイト列の16進数を続ける形式に変換する処理です。例えば、スペースは %20 に、?%3F に、#%23 に変換されます。これにより、URLが正しく解析され、意図したリソースにアクセスできるようになります。

net/http.FileServer

net/http.FileServer は、Go言語の net/http パッケージが提供するHTTPハンドラの一つで、ファイルシステム上の静的ファイルをHTTP経由で提供するために使用されます。ディレクトリがリクエストされた場合、FileServer はそのディレクトリ内のファイルとサブディレクトリの一覧を含むHTML形式のディレクトリインデックスを自動的に生成して返します。

html.EscapeStringhtmlReplacer

Go言語の標準ライブラリには、HTMLエスケープを行うための html パッケージがあります。その中の html.EscapeString 関数は、文字列をHTMLエスケープするために使用されます。 このコミットでは、html パッケージへの直接的な依存を避けるために、net/http/server.go 内で定義されている htmlReplacer という内部的なヘルパーを使用しています。この htmlReplacer は、実質的に html.EscapeString と同等の機能を提供します。これは、net/http パッケージが html パッケージに依存すると、循環依存や不必要な依存関係が生じるのを避けるための設計上の考慮事項です。

os.File.Readdirio.EOF

os.File.Readdir は、ディレクトリの内容を読み取り、os.FileInfo のスライスとして返すGoの標準ライブラリ関数です。count 引数に0より大きい値を指定すると、最大でその数のエントリを返します。すべてのエントリを読み終えると、io.EOF エラーを返します。このコミットでは、テスト用の fakeFile.Readdir の実装が、この os.File.Readdir の振る舞いをより正確にエミュレートするように修正されています。

技術的詳細

このコミットの主要な変更点は、net/http/fs.go 内の dirList 関数にあります。この関数は、FileServer がディレクトリインデックスを生成する際に、各ファイル/ディレクトリのエントリをHTMLの <a> タグとして出力します。

修正前は、ファイル名(name 変数)がそのまま href 属性と表示テキストの両方に使用されていました。

fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", name, name)

このコードには以下の問題がありました。

  1. href 属性のURLエンコーディング不足: name?# などのURL特殊文字が含まれている場合、これらがURLのパスの一部としてではなく、クエリパラメータやフラグメントの開始として解釈されてしまい、リンクが壊れる可能性がありました。
  2. 表示テキストのHTMLエスケープ不足: name<> などのHTML特殊文字が含まれている場合、これらがHTMLタグとして解釈されてしまい、HTMLインジェクション(XSS)の脆弱性につながる可能性がありました。

このコミットでは、これらの問題を解決するために以下の修正が行われました。

  1. URLエンコーディングの適用: url.URL 型を使用してファイル名をURLパスとして適切にエンコードしています。url.URL{Path: name} とすることで、name がURLパスとして解釈され、url.String() を呼び出すことで必要なURLエンコーディングが自動的に適用されます。これにより、?# などの文字が %3F%23 のようにエスケープされ、URLの破損が防がれます。
  2. HTMLエスケープの適用: 表示テキスト部分には htmlReplacer.Replace(name) を適用しています。htmlReplacernet/http パッケージ内部で定義されているHTMLエスケープ用の文字列リプレースであり、html.EscapeString と同等の機能を提供します。これにより、<> などのHTML特殊文字が &lt;&gt; のようにエスケープされ、HTMLインジェクションが防がれます。

修正後のコードは以下のようになります。

url := url.URL{Path: name}
fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", url.String(), htmlReplacer.Replace(name))

また、テストコード (src/pkg/net/http/fs_test.go) では、TestFileServerEscapesNames という新しいテスト関数が追加され、様々な特殊文字を含むファイル名が正しくエスケープされることを検証しています。これには、HTML特殊文字とURL特殊文字の両方が含まれています。

さらに、fakeFile.Readdir の実装も改善されています。これはテストハーネスの一部であり、os.File.Readdir の振る舞いをより正確にエミュレートするように変更されました。具体的には、count 引数に基づいてエントリを返し、すべてのエントリを読み終えた後に io.EOF を返すように修正されています。これにより、FileServer のディレクトリリスト機能のテストがより堅牢になります。

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

src/pkg/net/http/fs.go

--- a/src/pkg/net/http/fs.go
+++ b/src/pkg/net/http/fs.go
@@ -13,6 +13,7 @@ import (
 	"mime"
 	"mime/multipart"
 	"net/textproto"
+	"net/url" // 追加
 	"os"
 	"path"
 	"path/filepath"
@@ -75,8 +76,11 @@ func dirList(w ResponseWriter, f File) {
 			if d.IsDir() {
 				name += "/"
 			}
-			// TODO htmlescape // 削除
-			fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", name, name) // 変更前
+			// name may contain '?' or '#' を含む可能性があり、
+			// URLパスの一部として残るようにエスケープする必要がある。
+			// クエリ文字列やフラグメントの開始を示すものではない。
+			url := url.URL{Path: name} // 追加
+			fmt.Fprintf(w, "<a href=\"%s\">%s</a>\n", url.String(), htmlReplacer.Replace(name)) // 変更後
 		}
 	}
 	fmt.Fprintf(w, "</pre>\n")

src/pkg/net/http/fs_test.go

--- a/src/pkg/net/http/fs_test.go
+++ b/src/pkg/net/http/fs_test.go
@@ -227,6 +227,54 @@ func TestFileServerCleans(t *testing.T) {
 	}
 }
 
+// TestFileServerEscapesNames の追加
+func TestFileServerEscapesNames(t *testing.T) {
+	defer afterTest(t)
+	const dirListPrefix = "<pre>\n"
+	const dirListSuffix = "\n</pre>\n"
+	tests := []struct {
+		name, escaped string
+	}{
+		{`simple_name`, `<a href="simple_name">simple_name</a>`},
+		{`"'<>&`, `<a href="%22%27%3C%3E&">&#34;&#39;&lt;&gt;&amp;</a>`},
+		{`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
+		{`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo">&lt;combo&gt;?foo</a>`},
+	}
+
+	// 各テストファイルを独自のディレクトリに配置し、個別に確認できるようにする。
+	fs := make(fakeFS)
+	for i, test := range tests {
+		testFile := &fakeFileInfo{basename: test.name}
+		fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{
+			dir:     true,
+			modtime: time.Unix(1000000000, 0).UTC(),
+			ents:    []*fakeFileInfo{testFile},
+		}
+		fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile
+	}
+
+	ts := httptest.NewServer(FileServer(&fs))
+	defer ts.Close()
+	for i, test := range tests {
+		url := fmt.Sprintf("%s/%d", ts.URL, i)
+		res, err := Get(url)
+		if err != nil {
+			t.Fatalf("test %q: Get: %v", test.name, err)
+		}
+		b, err := ioutil.ReadAll(res.Body)
+		if err != nil {
+			t.Fatalf("test %q: read Body: %v", test.name, err)
+		}
+		s := string(b)
+		if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) {
+			t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix)
+		}
+		if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped {
+			t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped)
+		}
+		res.Body.Close()
+	}
+}
+
 func mustRemoveAll(dir string) {
 	err := os.RemoveAll(dir)
 	if err != nil {
@@ -457,19 +505,26 @@ func (f *fakeFileInfo) Mode() os.FileMode {
 
 type fakeFile struct {
 	io.ReadSeeker
-	fi   *fakeFileInfo
-	path string // as opened
+	fi     *fakeFileInfo
+	path   string // as opened
+	entpos int // 追加
 }
 
 func (f *fakeFile) Close() error               { return nil }
 func (f *fakeFile) Stat() (os.FileInfo, error) { return f.fi, nil }
 func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) {
 	if !f.fi.dir {
 		return nil, os.ErrInvalid
 	}
 	var fis []os.FileInfo
-	for _, fi := range f.fi.ents {
-		fis = append(fis, fi)
+
+	limit := f.entpos + count // 追加
+	if count <= 0 || limit > len(f.fi.ents) { // 追加
+		limit = len(f.fi.ents) // 追加
+	}
+	for ; f.entpos < limit; f.entpos++ { // 変更
+		fis = append(fis, f.fi.ents[f.entpos]) // 変更
 	}
-	return fis, nil
+
+	if len(fis) == 0 && count > 0 { // 追加
+		return fis, io.EOF // 追加
+	} else { // 追加
+		return fis, nil // 追加
+	}
 }
 
 type fakeFS map[string]*fakeFileInfo
@@ -480,7 +535,6 @@ func (fs fakeFS) Open(name string) (File, error) {
 	name = path.Clean(name)
 	f, ok := fs[name]
 	if !ok {
-		println("fake filesystem didn't find file", name) // 削除
 		return nil, os.ErrNotExist
 	}
 	return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil

コアとなるコードの解説

src/pkg/net/http/fs.go の変更

  • net/url パッケージのインポート: URLエンコーディングのために net/url パッケージが新しくインポートされました。
  • dirList 関数の修正:
    • 以前はファイル名 name が直接 <a> タグの href 属性と表示テキストに使用されていました。
    • 新しいコードでは、まず url.URL{Path: name} を使用して url.URL オブジェクトを作成しています。これにより、name がURLパスとして扱われ、url.String() を呼び出すことで自動的にURLエンコーディングが適用されます。例えば、?%3F に、#%23 に変換されます。
    • 表示テキスト部分には htmlReplacer.Replace(name) が適用されています。htmlReplacernet/http/server.go で定義されている内部的なヘルパーで、HTML特殊文字(<, >, &, ", ')を対応するHTML実体参照(&lt;, &gt;, &amp;, &quot;, &#39;)に変換します。これにより、HTMLインジェクションが防止されます。

src/pkg/net/http/fs_test.go の変更

  • TestFileServerEscapesNames 関数の追加:
    • この新しいテスト関数は、FileServer が生成するディレクトリインデックスにおいて、ファイル名が正しくエスケープされることを検証します。
    • 様々な特殊文字(HTML特殊文字、URL特殊文字)を含むファイル名がテストケースとして定義されています。
    • fakeFShttptest.NewServer を使用して、仮想ファイルシステムとHTTPサーバーをセットアップし、実際に FileServer が生成するHTML出力を取得します。
    • 取得したHTML出力からファイル名のエスケープ結果を抽出し、期待されるエスケープ結果と比較することで、修正が正しく機能していることを確認します。
  • fakeFile.Readdir の改善:
    • fakeFile はテスト目的で使用される os.File のモック実装です。
    • Readdir メソッドに entpos フィールドが追加され、現在読み取っているエントリの位置を追跡するようになりました。
    • count 引数に基づいて、指定された数のエントリを返すようにロジックが変更されました。
    • すべてのエントリを読み終えた後、io.EOF を返すように修正され、os.File.Readdir の標準的な振る舞いをより正確にエミュレートするようになりました。これにより、FileServer のディレクトリリスト機能のテストがより堅牢になります。
    • デバッグ用の println ステートメントが削除されました。

これらの変更により、net/http.FileServer はより安全になり、悪意のあるファイル名によるHTMLインジェクションやURL破損のリスクが軽減されました。

関連リンク

参考にした情報源リンク