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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおける Content-Type ヘッダの処理に関する改善です。具体的には、HTTPレスポンスの Content-Type ヘッダが明示的に設定されていない場合に、ファイルの内容から Content-Type を推測(スニッフィング)する挙動を制御するための変更が加えられています。これにより、開発者が Content-Type を意図的に空にしたい場合に、ライブラリが勝手に推測して設定してしまう問題を解決します。

コミット

commit 21e6b90d36db8d10e93ca281aee404b5f7720f48
Author: Michael Piatek <piatek@google.com>
Date:   Tue Oct 15 08:22:04 2013 +1100

    net/http: skip content-type sniffing if the header is explicitly unset.
    
    Fixes #5953
    
    R=dsymonds, bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/14434044

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

https://github.com/golang/go/commit/21e6b90d36db8d10e93ca281aee404b5f7720f48

元コミット内容

net/http: skip content-type sniffing if the header is explicitly unset.

Fixes #5953

R=dsymonds, bradfitz, rsc CC=golang-dev https://golang.org/cl/14434044

変更の背景

この変更は、Goの net/http パッケージが提供するファイルサービング機能(ServeFileServeContent など)において、HTTPレスポンスの Content-Type ヘッダの挙動に関する問題を修正するために導入されました。

従来の net/http パッケージでは、レスポンスヘッダに Content-Type が設定されていない場合、Goは自動的にファイルの内容や拡張子に基づいて Content-Type を推測(スニッフィング)し、ヘッダに設定していました。これは多くの場合において便利な機能ですが、開発者が意図的に Content-Type ヘッダを空にしたい、あるいは特定の状況下で Content-Type の自動推測を避けたい場合に問題を引き起こしていました。

具体的には、GoのIssue #5953で報告された問題が背景にあります。このIssueでは、Content-Type ヘッダを明示的に空の文字列として設定しても、net/http がそれを無視してファイルの内容から Content-Type を推測し、ヘッダを上書きしてしまうという挙動が指摘されました。例えば、セキュリティ上の理由から Content-Type を意図的に非公開にしたい場合や、カスタムの Content-Type 処理ロジックを持つ場合に、この自動スニッフィングが邪魔になることがありました。

このコミットは、このような開発者の意図を尊重し、Content-Type ヘッダが明示的に空の文字列として設定された場合には、自動的なスニッフィングを行わないように挙動を変更することで、より柔軟な制御を可能にすることを目的としています。

前提知識の解説

HTTP Content-Type ヘッダ

HTTPの Content-Type ヘッダは、HTTPメッセージのエンティティボディに含まれるデータのメディアタイプ(MIMEタイプ)を示すために使用されます。これにより、クライアント(ブラウザなど)は受信したデータの種類を理解し、適切に処理(表示、再生など)することができます。

例:

  • text/html: HTMLドキュメント
  • application/json: JSONデータ
  • image/png: PNG画像
  • text/plain; charset=utf-8: UTF-8エンコードされたプレーンテキスト

Content-Type スニッフィング

Content-Type スニッフィング(MIMEスニッフィングとも呼ばれる)は、HTTPレスポンスに Content-Type ヘッダが指定されていない場合や、指定された Content-Type が不明確な場合に、ブラウザやその他のクライアントが受信したデータのバイト列を検査して、その実際のメディアタイプを推測するプロセスです。

これは、ウェブサーバーが常に正しい Content-Type を設定するとは限らないため、ユーザーエクスペリエンスを向上させるために導入されました。しかし、悪意のあるユーザーがアップロードしたファイルが、意図しない Content-Type として解釈され、クロスサイトスクリプティング(XSS)などのセキュリティ脆弱性を引き起こす可能性もあります。そのため、サーバー側で X-Content-Type-Options: nosniff ヘッダを設定して、ブラウザのスニッフィングを抑制することが推奨される場合もあります。

Goの net/http パッケージでは、ServeFileServeContent 関数が、デフォルトで Content-Type ヘッダが設定されていない場合に、ファイルの内容を読み取って Content-Type を推測する機能を持っています。このコミットは、この自動スニッフィングの挙動をより細かく制御できるようにするためのものです。

Go言語の net/http パッケージ

net/http パッケージは、Go言語でHTTPクライアントおよびサーバーを実装するための基本的な機能を提供します。これには、HTTPリクエストの処理、レスポンスの生成、ルーティング、ミドルウェアのサポートなどが含まれます。

  • http.ResponseWriter: HTTPレスポンスを構築するためのインターフェース。ヘッダの設定やボディの書き込みを行います。
  • http.Request: 受信したHTTPリクエストを表す構造体。
  • http.ServeFile(w ResponseWriter, r *Request, name string): 指定されたパスのファイルをHTTPレスポンスとして提供するヘルパー関数。
  • http.ServeContent(w ResponseWriter, r *Request, name string, modtime time.Time, content io.ReadSeeker): ServeFile よりも低レベルで、io.ReadSeeker インターフェースを実装する任意のコンテンツを提供できる関数。

Goの Header マップと Get メソッド

Goの http.Headermap[string][]string 型であり、HTTPヘッダのキーと値のペアを管理します。

  • w.Header().Get("Content-Type"): ヘッダマップから指定されたキーの最初の値を取得します。キーが存在しない場合や、値が空のスライスの場合、空文字列 "" を返します。
  • w.Header()["Content-Type"]: ヘッダマップから直接スライス []string を取得します。この方法では、キーが存在するかどうか(haveType)と、スライスが空かどうかを区別できます。

この違いが、今回のコミットの核心部分となります。Get メソッドでは、ヘッダが存在しない場合と、ヘッダが空のスライスとして明示的に設定されている場合を区別できませんでした。

技術的詳細

このコミットの主要な変更点は、net/http パッケージ内の serveContent 関数と response 構造体の needsSniff メソッドにおける Content-Type ヘッダのチェックロジックです。

serveContent 関数の変更

serveContent 関数は、ファイルやその他のコンテンツをHTTPレスポンスとして提供する際に、Content-Type ヘッダの処理を担当します。

変更前:

	// If Content-Type isn't set, use the file's extension to find it.
	ctype := w.Header().Get("Content-Type")
	if ctype == "" {
		// ... Content-Type sniffing logic ...
		w.Header().Set("Content-Type", ctype)
	}

変更前は、w.Header().Get("Content-Type") の結果が空文字列 "" であれば、Content-Type が設定されていないと判断し、スニッフィングを行っていました。しかし、Get メソッドは、ヘッダが全く存在しない場合と、w.Header()["Content-Type"] = []string{} のように明示的に空のスライスとして設定された場合の両方で "" を返します。このため、開発者が Content-Type を意図的に空にしたい場合でも、スニッフィングが実行されてしまっていました。

変更後:

	// If Content-Type isn't set, use the file's extension to find it, but
	// if the Content-Type is unset explicitly, do not sniff the type.
	ctypes, haveType := w.Header()["Content-Type"]
	var ctype string
	if !haveType {
		// ... Content-Type sniffing logic ...
		w.Header().Set("Content-Type", ctype)
	} else if len(ctypes) > 0 {
		ctype = ctypes[0]
	}

変更後では、w.Header().Get("Content-Type") の代わりに、ctypes, haveType := w.Header()["Content-Type"] を使用して、Content-Type ヘッダがマップ内に存在するかどうか(haveType)を直接チェックしています。

  • !haveType: Content-Type ヘッダが全く設定されていない場合。この場合にのみ、ファイルの内容や拡張子に基づく Content-Type のスニッフィングが行われます。
  • haveType かつ len(ctypes) == 0: Content-Type ヘッダが明示的に空のスライス(例: w.Header()["Content-Type"] = []string{})として設定されている場合。この場合、!haveTypefalse となり、スニッフィングはスキップされます。これにより、開発者が Content-Type を意図的に空に保つことができるようになります。
  • haveType かつ len(ctypes) > 0: Content-Type ヘッダが既に値を持って設定されている場合。この場合もスニッフィングは行われず、既存の値が使用されます。

response.needsSniff メソッドの変更

response.needsSniff メソッドは、HTTPレスポンスの書き込み中に Content-Type のスニッフィングが必要かどうかを判断するために使用されます。

変更前:

func (w *response) needsSniff() bool {
	return !w.cw.wroteHeader && w.handlerHeader.Get("Content-Type") == "" && w.written < sniffLen
}

ここでも w.handlerHeader.Get("Content-Type") == "" が使用されており、Content-Type が存在しない場合と明示的に空の場合を区別できませんでした。

変更後:

func (w *response) needsSniff() bool {
	_, haveType := w.handlerHeader["Content-Type"]
	return !w.cw.wroteHeader && !haveType && w.written < sniffLen
}

変更後では、w.handlerHeader.Get("Content-Type") == "" の代わりに !haveType を使用しています。これにより、Content-Type ヘッダがマップ内に存在しない場合にのみ needsSnifftrue を返すようになり、明示的に空に設定された場合にはスニッフィングが抑制されます。

テストの追加と修正

この変更の挙動を検証するために、fs_test.goresponse_test.go にテストが追加・修正されています。

  • TestServeFileContentType では、Content-Type を明示的に空に設定した場合にスニッフィングが行われないことを確認する新しいテストケースが追加されました。
  • TestNeedsSniff では、Content-Typenil(つまり、w.Header()["Content-Type"] = nil のように設定された場合)のときに needsSnifffalse を返すことを確認するテストが追加されました。これは、Content-Type ヘッダがマップ内に存在しない場合(!haveType)と、キーは存在するが値が nil の場合(haveTypetrue だが len(ctypes)0)の挙動を明確にするものです。ただし、w.Header()["Content-Type"] = nilw.Header().Del("Content-Type") と同等であり、ヘッダがマップから削除されるため、haveTypefalse になります。したがって、このテストは needsSniff の変更後のロジックと整合しています。

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

src/pkg/net/http/fs.go

--- a/src/pkg/net/http/fs.go
+++ b/src/pkg/net/http/fs.go
@@ -140,9 +140,11 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
 
 	code := StatusOK
 
-	// If Content-Type isn't set, use the file's extension to find it.
-	ctype := w.Header().Get("Content-Type")
-	if ctype == "" {
+	// If Content-Type isn't set, use the file's extension to find it, but
+	// if the Content-Type is unset explicitly, do not sniff the type.
+	ctypes, haveType := w.Header()["Content-Type"]
+	var ctype string
+	if !haveType {
 		ctype = mime.TypeByExtension(filepath.Ext(name))
 		if ctype == "" {
 			// read a chunk to decide between utf-8 text and binary
@@ -156,6 +158,8 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,\
 			}
 		}
 		w.Header().Set("Content-Type", ctype)
+	} else if len(ctypes) > 0 {
+		ctype = ctypes[0]
 	}
 
 	size, err := sizeFunc()

src/pkg/net/http/fs_test.go

--- a/src/pkg/net/http/fs_test.go
+++ b/src/pkg/net/http/fs_test.go
@@ -20,6 +20,7 @@ import (
 	"os/exec"
 	"path"
 	"path/filepath"
+	"reflect"
 	"regexp"
 	"runtime"
 	"strconv"
@@ -319,24 +320,29 @@ func TestServeFileContentType(t *testing.T) {
 	defer afterTest(t)
 	const ctype = "icecream/chocolate"
 	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
-		if r.FormValue("override") == "1" {
+		switch r.FormValue("override") {
+		case "1":
 			w.Header().Set("Content-Type", ctype)
+		case "2":
+			// Explicitly inhibit sniffing.
+			w.Header()["Content-Type"] = []string{}
 		}
 		ServeFile(w, r, "testdata/file")
 	}))
 	defer ts.Close()
-	get := func(override, want string) {
+	get := func(override string, want []string) {
 		resp, err := Get(ts.URL + "?override=" + override)
 		if err != nil {
 			t.Fatal(err)
 		}
-		if h := resp.Header.Get("Content-Type"); h != want {
-			t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
+		if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
+			t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
 		}
 		resp.Body.Close()
 	}
-	get("0", "text/plain; charset=utf-8")
-	get("1", ctype)
+	get("0", []string{"text/plain; charset=utf-8"})
+	get("1", []string{ctype})
+	get("2", nil)
 }
 
 func TestServeFileMimeType(t *testing.T) {

src/pkg/net/http/response_test.go

--- a/src/pkg/net/http/response_test.go
+++ b/src/pkg/net/http/response_test.go
@@ -614,3 +614,16 @@ func TestResponseContentLengthShortBody(t *testing.T) {\n \t\tt.Errorf("io.Copy error = %#v; want io.ErrUnexpectedEOF", err)\n \t}\n }\n+\n+func TestNeedsSniff(t *testing.T) {\n+\t// needsSniff returns true with an empty response.\n+\tr := &response{}\n+\tif got, want := r.needsSniff(), true; got != want {\n+\t\tt.Errorf("needsSniff = %t; want %t", got, want)\n+\t}\n+\t// needsSniff returns false when Content-Type = nil.\n+\tr.handlerHeader = Header{"Content-Type": nil}\n+\tif got, want := r.needsSniff(), false; got != want {\n+\t\tt.Errorf("needsSniff empty Content-Type = %t; want %t", got, want)\n+\t}\n+}\n```

### `src/pkg/net/http/server.go`

```diff
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -341,7 +341,8 @@ func (w *response) requestTooLarge() {\n \n // needsSniff reports whether a Content-Type still needs to be sniffed.\n func (w *response) needsSniff() bool {\n-\treturn !w.cw.wroteHeader && w.handlerHeader.Get("Content-Type") == "" && w.written < sniffLen\n+\t_, haveType := w.handlerHeader["Content-Type"]\n+\treturn !w.cw.wroteHeader && !haveType && w.written < sniffLen\n }\n \n // writerOnly hides an io.Writer value's optional ReadFrom method

コアとなるコードの解説

src/pkg/net/http/fs.goserveContent 関数

この変更の最も重要な部分は、serveContent 関数における Content-Type の決定ロジックです。

  • ctypes, haveType := w.Header()["Content-Type"]: ここが変更の核心です。w.Header()map[string][]string 型の Header を返します。マップから直接キー "Content-Type" を取得することで、そのキーがマップ内に存在するかどうかを haveType ブール値で正確に判断できます。
    • haveTypefalse の場合(Content-Type ヘッダが全く設定されていない場合): 従来のロジックと同様に、mime.TypeByExtension やファイル内容のスニッフィングによって Content-Type を推測し、w.Header().Set("Content-Type", ctype) で設定します。
    • haveTypetrue の場合(Content-Type ヘッダがマップ内に存在する場合):
      • len(ctypes) > 0: Content-Type ヘッダに値が設定されている場合(例: Content-Type: text/html)。この場合、既存の ctypes[0]ctype として使用し、スニッフィングは行いません。
      • len(ctypes) == 0: Content-Type ヘッダが明示的に空のスライスとして設定されている場合(例: w.Header()["Content-Type"] = []string{})。この場合、else if len(ctypes) > 0 の条件は満たされないため、ctype は初期値の空文字列のままとなり、スニッフィングも行われません。これにより、開発者が Content-Type を意図的に空に保つことができるようになります。

src/pkg/net/http/server.goresponse.needsSniff メソッド

needsSniff メソッドは、レスポンスヘッダがまだ書き込まれておらず、かつ Content-Type が設定されていない場合に true を返します。

  • _, haveType := w.handlerHeader["Content-Type"]: ここでも serveContent と同様に、Content-Type ヘッダがマップ内に存在するかどうかを haveType で直接チェックします。
  • !haveType: Content-Type ヘッダがマップ内に存在しない場合にのみ、スニッフィングが必要であると判断します。これにより、明示的に空に設定された Content-Type ヘッダに対してはスニッフィングが行われなくなります。

テストファイルの変更

  • src/pkg/net/http/fs_test.go:

    • switch r.FormValue("override"): override パラメータの値に応じて、Content-Type を設定するか、明示的に空にするかのロジックが追加されました。
    • w.Header()["Content-Type"] = []string{}: override="2" の場合に、Content-Type ヘッダを明示的に空のスライスとして設定しています。これが、スニッフィングを抑制する新しい挙動をテストするための重要な部分です。
    • get := func(override string, want []string): テストヘルパー関数 get のシグネチャが変更され、期待される Content-Type[]string 型になりました。これは、resp.Header["Content-Type"][]string を返すため、より正確な比較を行うためです。
    • !reflect.DeepEqual(h, want): ヘッダの値がスライスであるため、reflect.DeepEqual を使用してスライス全体が等しいかを比較しています。
    • get("2", nil): 新しいテストケースで、override="2" の場合に Content-Type ヘッダが nil(つまり、レスポンスヘッダに Content-Type が存在しない状態)であることを期待しています。これは、w.Header()["Content-Type"] = []string{} と設定した場合、net/http の内部処理で最終的にヘッダが削除されるためです。
  • src/pkg/net/http/response_test.go:

    • TestNeedsSniff: response 構造体の needsSniff メソッドの挙動をテストする新しいテスト関数が追加されました。
    • r.handlerHeader = Header{"Content-Type": nil}: Content-Type ヘッダが nil として設定された場合に needsSnifffalse を返すことを確認しています。これは、Content-Type ヘッダがマップ内に存在しない状態と同等と見なされるためです。

これらの変更により、Goの net/http パッケージは、開発者が Content-Type ヘッダを明示的に空にしたいという意図を尊重し、不要な自動スニッフィングを抑制できるようになりました。

関連リンク

参考にした情報源リンク