[インデックス 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
以前は、特殊文字を含むファイル名が以下の問題を引き起こす可能性がありました。
<a>
タグをエスケープする(例:">foo
というファイル名)- URLのパス部分を途中で終了させることで、インデックス内のリンクを破壊する(例:
foo?bar
というファイル名)
html
パッケージへの不必要な依存を避けるため、net/http/server.go
の htmlReplacer
を使用しています。これは html.EscapeString
と同等です。
この変更は、fakeFile.Readdir
を os.File.Readdir
をより良くエミュレートするように拡張しています。
変更の背景
このコミットの背景には、Webアプリケーションにおける一般的なセキュリティ脆弱性であるHTMLインジェクション(またはクロスサイトスクリプティング - XSS)の防止があります。net/http
パッケージの FileServer
は、ディレクトリの内容を一覧表示する際に、ファイル名をHTMLとして出力します。このとき、ファイル名にHTMLの特殊文字(<
, >
, &
, "
, '
など)やURLの特殊文字(?
, #
など)が含まれていると、以下のような問題が発生する可能性がありました。
- HTMLインジェクション: 悪意のあるユーザーが、例えばファイル名を
"><script>alert('XSS')</script>
のように作成した場合、FileServer
がこれをエスケープせずにHTMLに出力すると、ブラウザはそのスクリプトを実行してしまいます。これにより、セッションハイジャック、情報の盗難、サイトの改ざんなど、様々な攻撃が可能になります。 - URLの破損: ファイル名に
?
や#
といったURLのクエリパラメータやフラグメント識別子として解釈される文字が含まれている場合、FileServer
が生成するリンクが意図しないURLになってしまい、リンクが機能しなくなるか、誤ったリソースを指す可能性がありました。
このコミットは、これらの脆弱性を解消し、FileServer
がより安全にディレクトリインデックスを提供できるようにするために導入されました。
前提知識の解説
HTMLエスケープ
HTMLエスケープとは、HTMLドキュメント内で特殊な意味を持つ文字(例: <
はタグの開始、>
はタグの終了、&
は実体参照の開始)を、その文字自体として表示させるために、別の表現(実体参照)に変換する処理のことです。例えば、<
は <
に、>
は >
に、&
は &
に、"
は "
に、'
は '
または '
に変換されます。これにより、ブラウザがこれらの文字を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.EscapeString
と htmlReplacer
Go言語の標準ライブラリには、HTMLエスケープを行うための html
パッケージがあります。その中の html.EscapeString
関数は、文字列をHTMLエスケープするために使用されます。
このコミットでは、html
パッケージへの直接的な依存を避けるために、net/http/server.go
内で定義されている htmlReplacer
という内部的なヘルパーを使用しています。この htmlReplacer
は、実質的に html.EscapeString
と同等の機能を提供します。これは、net/http
パッケージが html
パッケージに依存すると、循環依存や不必要な依存関係が生じるのを避けるための設計上の考慮事項です。
os.File.Readdir
と io.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)
このコードには以下の問題がありました。
href
属性のURLエンコーディング不足:name
に?
や#
などのURL特殊文字が含まれている場合、これらがURLのパスの一部としてではなく、クエリパラメータやフラグメントの開始として解釈されてしまい、リンクが壊れる可能性がありました。- 表示テキストのHTMLエスケープ不足:
name
に<
や>
などのHTML特殊文字が含まれている場合、これらがHTMLタグとして解釈されてしまい、HTMLインジェクション(XSS)の脆弱性につながる可能性がありました。
このコミットでは、これらの問題を解決するために以下の修正が行われました。
- URLエンコーディングの適用:
url.URL
型を使用してファイル名をURLパスとして適切にエンコードしています。url.URL{Path: name}
とすることで、name
がURLパスとして解釈され、url.String()
を呼び出すことで必要なURLエンコーディングが自動的に適用されます。これにより、?
や#
などの文字が%3F
や%23
のようにエスケープされ、URLの破損が防がれます。 - HTMLエスケープの適用: 表示テキスト部分には
htmlReplacer.Replace(name)
を適用しています。htmlReplacer
はnet/http
パッケージ内部で定義されているHTMLエスケープ用の文字列リプレースであり、html.EscapeString
と同等の機能を提供します。これにより、<
や>
などのHTML特殊文字が<
や>
のようにエスケープされ、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&">"'<>&</a>`},
+ {`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
+ {`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo"><combo>?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)
が適用されています。htmlReplacer
はnet/http/server.go
で定義されている内部的なヘルパーで、HTML特殊文字(<
,>
,&
,"
,'
)を対応するHTML実体参照(<
,>
,&
,"
,'
)に変換します。これにより、HTMLインジェクションが防止されます。
- 以前はファイル名
src/pkg/net/http/fs_test.go
の変更
TestFileServerEscapesNames
関数の追加:- この新しいテスト関数は、
FileServer
が生成するディレクトリインデックスにおいて、ファイル名が正しくエスケープされることを検証します。 - 様々な特殊文字(HTML特殊文字、URL特殊文字)を含むファイル名がテストケースとして定義されています。
fakeFS
とhttptest.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破損のリスクが軽減されました。
関連リンク
- Go言語
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - Go言語
net/url
パッケージのドキュメント: https://pkg.go.dev/net/url - Go言語
html
パッケージのドキュメント: https://pkg.go.dev/html - Go言語
os
パッケージのドキュメント: https://pkg.go.dev/os
参考にした情報源リンク
- Go CL 37440043: https://golang.org/cl/37440043
- HTMLエスケープに関する一般的な情報 (OWASPなど): https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- URLエンコーディングに関する一般的な情報: https://ja.wikipedia.org/wiki/パーセントエンコーディング
- Go言語の
htmlReplacer
の実装に関する情報 (Goのソースコード): https://github.com/golang/go/blob/master/src/net/http/server.go (このコミット時点のバージョンとは異なる可能性があります)