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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおけるMIMEタイプ検出の挙動を修正するものです。具体的には、HTTPハンドラが Content-Type ヘッダを空文字列で明示的に設定した場合に、net/http がMIMEスニッフィングを行わないように変更します。

変更されたファイルは以下の通りです。

  • src/pkg/net/http/server.go: HTTPサーバーのレスポンス処理ロジックが変更されました。
  • src/pkg/net/http/sniff_test.go: MIMEスニッフィングの挙動に関する新しいテストケースが追加されました。

コミット

  • コミットハッシュ: 252c107f2fe3e1ba8a58e06ec0e63fa8c8f90bb5
  • Author: Brad Fitzpatrick bradfitz@golang.org
  • Date: Wed Jul 31 23:38:32 2013 -0700

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

https://github.com/golang/go/commit/252c107f2fe3e1ba8a58e06ec0e63fa8c8f90bb5

元コミット内容

net/http: don't MIME sniff if handler set an empty string Content-Type

Fixes #5953

R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/12117043

変更の背景

この変更は、Goの net/http パッケージが抱えていたMIMEスニッフィングに関する特定のバグ、Issue 5953を修正するために行われました。

従来の net/http の実装では、HTTPレスポンスの Content-Type ヘッダが空文字列 ("") であった場合、または全く設定されていなかった場合に、レスポンスボディの内容に基づいてMIMEタイプを自動的に検出(MIMEスニッフィング)する挙動がありました。これは、ハンドラが Content-Type を明示的に設定しなかった場合に、ブラウザがコンテンツを正しく解釈できるようにするためのフォールバックメカニズムとして意図されていました。

しかし、ハンドラが意図的に Content-Type: (空文字列) を設定した場合でも、このMIMEスニッフィングが発動してしまうという問題がありました。これは、ハンドラが「Content-Typeは指定しない」という明確な意図を示しているにもかかわらず、システムがそれを無視して自動検出を行ってしまうため、予期せぬ挙動やセキュリティ上の問題を引き起こす可能性がありました。例えば、ブラウザがMIMEスニッフィングによってHTMLやJavaScriptとして解釈し、XSS(クロスサイトスクリプティング)などの脆弱性につながる可能性も考えられます。

このコミットは、ハンドラが Content-Type ヘッダを空文字列で明示的に設定した場合、その意図を尊重し、MIMEスニッフィングを行わないようにすることで、この問題を解決します。

前提知識の解説

MIMEタイプ (Media Type)

MIMEタイプ(またはメディアタイプ)は、インターネット上で転送されるデータの種類を識別するための標準的な方法です。HTTPレスポンスでは、Content-Type ヘッダとしてクライアント(ブラウザなど)に送信され、クライアントはその情報に基づいてコンテンツをどのように処理すべきかを判断します。

例:

  • text/html: HTMLドキュメント
  • application/json: JSONデータ
  • image/png: PNG画像
  • application/octet-stream: バイナリデータ(不明なタイプ)

MIMEスニッフィング (MIME Sniffing)

MIMEスニッフィングは、HTTPレスポンスに Content-Type ヘッダが含まれていない場合や、その値が一般的なMIMEタイプではない場合に、ブラウザなどのクライアントがレスポンスボディの先頭バイトを検査して、コンテンツの実際のMIMEタイプを推測するメカニズムです。

これは、サーバーが Content-Type ヘッダを適切に設定しない場合でも、ユーザーがコンテンツを閲覧できるようにするための「おせっかい」な機能として導入されました。しかし、この機能はセキュリティ上のリスクを伴うことがあります。例えば、ユーザーがアップロードしたファイルが、サーバー側で Content-Type が適切に設定されずに提供された場合、悪意のあるスクリプトが埋め込まれたファイルがHTMLとして解釈され、XSS攻撃につながる可能性があります。

X-Content-Type-Options: nosniff ヘッダ

MIMEスニッフィングによるセキュリティリスクを軽減するために、HTTPレスポンスには X-Content-Type-Options: nosniff ヘッダを含めることが推奨されています。このヘッダが存在する場合、対応するブラウザはMIMEスニッフィングを無効にし、サーバーが指定した Content-Type ヘッダを厳密に解釈します。これにより、意図しないMIMEタイプの解釈を防ぎ、セキュリティを向上させることができます。Goの net/http パッケージも、このヘッダを自動的に含める機能を持っています(Issue #24513で議論されました)。

Goの net/http パッケージにおけるMIMEスニッフィング

Goの net/http パッケージでは、http.DetectContentType 関数を使用してMIMEスニッフィングを行います。この関数は、レスポンスボディの最初の512バイトを調べて、適切なMIMEタイプを推測します。もし適切なMIMEタイプを特定できない場合、デフォルトで application/octet-stream を返します。

このコミット以前は、Content-Type ヘッダが全く設定されていない場合 (header.get("Content-Type") == "") に加えて、Content-Type ヘッダが空文字列で明示的に設定されている場合も、MIMEスニッフィングが実行されていました。

技術的詳細

このコミットの核心は、net/http パッケージ内の server.go ファイルにある chunkWriterwriteHeader メソッドの変更です。このメソッドは、HTTPレスポンスヘッダを書き込む際に、Content-Type ヘッダの有無と値に基づいてMIMEスニッフィングを行うかどうかを決定します。

変更前は、以下の条件でMIMEスニッフィングが実行されていました。

if header.get("Content-Type") == "" && w.req.Method != "HEAD" {
    setHeader.contentType = DetectContentType(p)
}

この条件は、Content-Type ヘッダが「存在しない」場合と、「空文字列である」場合の両方をカバーしていました。しかし、ハンドラが w.Header().Set("Content-Type", "") のように明示的に空文字列を設定した場合でも、この条件が真となり、MIMEスニッフィングが発動してしまいました。これは、ハンドラの意図(Content-Typeを明示的に空にする)に反する挙動でした。

このコミットでは、この条件を以下のように変更しました。

_, haveType := header["Content-Type"]
if !haveType && w.req.Method != "HEAD" {
    setHeader.contentType = DetectContentType(p)
}

この変更により、MIMEスニッフィングが実行されるのは、Content-Type ヘッダが全く存在しない場合のみとなりました。header["Content-Type"]map[string][]string 型であり、Goのマップアクセスでは、キーが存在しない場合にゼロ値と false が返されます。haveType は、Content-Type キーがマップ内に存在するかどうかを示すブール値です。したがって、!haveTypeContent-Type ヘッダが設定されていない場合にのみ真となります。

ハンドラが w.Header().Set("Content-Type", "") を呼び出した場合、header["Content-Type"][]string{""} となり、haveTypetrue になります。これにより、!haveType は偽となり、MIMEスニッフィングは実行されなくなります。

この変更は、ハンドラの意図を尊重し、Content-Type ヘッダが明示的に空文字列に設定された場合には、net/http が自動的なMIMEタイプ検出を行わないようにすることで、より予測可能で安全な挙動を実現します。

また、sniff_test.goTestServerIssue5953 という新しいテストケースが追加されました。このテストは、ハンドラが Content-Type ヘッダを空文字列に設定した場合に、レスポンスの Content-Type ヘッダが期待通り空文字列のままであり、MIMEスニッフィングが行われていないことを検証します。これにより、修正が正しく機能していることが保証されます。

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

diff --git a/src/pkg/net/http/server.go b/src/pkg/net/http/server.go
index e0f629347e..5332239ede 100644
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -792,7 +792,8 @@ func (cw *chunkWriter) writeHeader(p []byte) {
 		}
 	} else {
 		// If no content type, apply sniffing algorithm to body.
-		if header.get("Content-Type") == "" && w.req.Method != "HEAD" {
+		_, haveType := header["Content-Type"]
+		if !haveType && w.req.Method != "HEAD" {
 			setHeader.contentType = DetectContentType(p)
 		}
 	}
diff --git a/src/pkg/net/http/sniff_test.go b/src/pkg/net/http/sniff_test.go
index 106d94ec1c..24ca27afc1 100644
--- a/src/pkg/net/http/sniff_test.go
+++ b/src/pkg/net/http/sniff_test.go
@@ -12,6 +12,7 @@ import (
 	"log"
 	. "net/http"
 	"net/http/httptest"
+	"reflect"
 	"strconv"
 	"strings"
 	"testing"
@@ -84,6 +85,29 @@ func TestServerContentType(t *testing.T) {
 	}
 }
 
+// Issue 5953: shouldn't sniff if the handler set a Content-Type header,
+// even if it's the empty string.
+func TestServerIssue5953(t *testing.T) {
+	defer afterTest(t)
+	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+		w.Header()["Content-Type"] = []string{""}
+		fmt.Fprintf(w, "<html><head></head><body>hi</body></html>")
+	}))
+	defer ts.Close()
+
+	resp, err := Get(ts.URL)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	got := resp.Header["Content-Type"]
+	want := []string{""}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("Content-Type = %q; want %q", got, want)
+	}
+	resp.Body.Close()
+}
+
 func TestContentTypeWithCopy(t *testing.T) {
 	defer afterTest(t)
 

コアとなるコードの解説

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

変更の核心は、chunkWriter 構造体の writeHeader メソッド内の条件分岐です。

  • 変更前:

    if header.get("Content-Type") == "" && w.req.Method != "HEAD" {
        setHeader.contentType = DetectContentType(p)
    }
    

    header.get("Content-Type") は、Content-Type ヘッダの値を取得します。このメソッドは、ヘッダが存在しない場合も空文字列を返します。そのため、この条件は「Content-Type ヘッダが存在しないか、または空文字列である」場合に真となっていました。

  • 変更後:

    _, haveType := header["Content-Type"]
    if !haveType && w.req.Method != "HEAD" {
        setHeader.contentType = DetectContentType(p)
    }
    

    _, haveType := header["Content-Type"] の行では、Goのマップアクセスにおける「カンマOK」イディオムが使用されています。headermap[string][]string 型であり、header["Content-Type"]Content-Type キーに対応する値([]string 型)と、そのキーがマップ内に存在するかどうかを示すブール値 haveType を返します。 !haveType は、「Content-Type ヘッダがマップ内に存在しない」場合にのみ真となります。これにより、ハンドラが w.Header().Set("Content-Type", "") のように明示的に空文字列を設定した場合(この場合 haveTypetrue になる)、MIMEスニッフィングは実行されなくなります。

この変更により、ハンドラが Content-Type ヘッダを明示的に設定した場合は、その値が空文字列であってもMIMEスニッフィングが行われなくなり、ハンドラの意図が尊重されるようになりました。

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

TestServerIssue5953 という新しいテスト関数が追加されました。

func TestServerIssue5953(t *testing.T) {
    defer afterTest(t)
    ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
        w.Header()["Content-Type"] = []string{""} // ここでContent-Typeを空文字列に設定
        fmt.Fprintf(w, "<html><head></head><body>hi</body></html>")
    }))
    defer ts.Close()

    resp, err := Get(ts.URL)
    if err != nil {
        t.Fatal(err)
    }

    got := resp.Header["Content-Type"]
    want := []string{""} // 期待されるContent-Typeは空文字列のまま
    if !reflect.DeepEqual(got, want) {
        t.Errorf("Content-Type = %q; want %q", got, want)
    }
    resp.Body.Close()
}

このテストは以下のことを検証しています。

  1. httptest.NewServer を使用してテスト用のHTTPサーバーを起動します。
  2. ハンドラ内で w.Header()["Content-Type"] = []string{""} を呼び出し、Content-Type ヘッダを明示的に空文字列に設定します。
  3. レスポンスボディとしてHTMLコンテンツを書き込みます。
  4. クライアントからこのサーバーにHTTP GETリクエストを送信します。
  5. レスポンスヘッダから Content-Type を取得し、それが期待通り []string{""} であることを reflect.DeepEqual を使って検証します。

このテストが成功するということは、ハンドラが Content-Type を空文字列に設定した場合に、net/http がMIMEスニッフィングを行わず、設定された空文字列の Content-Type がそのままクライアントに返されていることを意味します。これにより、Issue 5953で報告された問題が解決されたことが確認できます。

関連リンク

参考にした情報源リンク