[インデックス 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
ファイルにある chunkWriter
の writeHeader
メソッドの変更です。このメソッドは、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
キーがマップ内に存在するかどうかを示すブール値です。したがって、!haveType
は Content-Type
ヘッダが設定されていない場合にのみ真となります。
ハンドラが w.Header().Set("Content-Type", "")
を呼び出した場合、header["Content-Type"]
は []string{""}
となり、haveType
は true
になります。これにより、!haveType
は偽となり、MIMEスニッフィングは実行されなくなります。
この変更は、ハンドラの意図を尊重し、Content-Type
ヘッダが明示的に空文字列に設定された場合には、net/http
が自動的なMIMEタイプ検出を行わないようにすることで、より予測可能で安全な挙動を実現します。
また、sniff_test.go
に TestServerIssue5953
という新しいテストケースが追加されました。このテストは、ハンドラが 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」イディオムが使用されています。header
はmap[string][]string
型であり、header["Content-Type"]
はContent-Type
キーに対応する値([]string
型)と、そのキーがマップ内に存在するかどうかを示すブール値haveType
を返します。!haveType
は、「Content-Type
ヘッダがマップ内に存在しない」場合にのみ真となります。これにより、ハンドラがw.Header().Set("Content-Type", "")
のように明示的に空文字列を設定した場合(この場合haveType
はtrue
になる)、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()
}
このテストは以下のことを検証しています。
httptest.NewServer
を使用してテスト用のHTTPサーバーを起動します。- ハンドラ内で
w.Header()["Content-Type"] = []string{""}
を呼び出し、Content-Type
ヘッダを明示的に空文字列に設定します。 - レスポンスボディとしてHTMLコンテンツを書き込みます。
- クライアントからこのサーバーにHTTP GETリクエストを送信します。
- レスポンスヘッダから
Content-Type
を取得し、それが期待通り[]string{""}
であることをreflect.DeepEqual
を使って検証します。
このテストが成功するということは、ハンドラが Content-Type
を空文字列に設定した場合に、net/http
がMIMEスニッフィングを行わず、設定された空文字列の Content-Type
がそのままクライアントに返されていることを意味します。これにより、Issue 5953で報告された問題が解決されたことが確認できます。
関連リンク
- Go Issue 5953: https://code.google.com/p/go/issues/detail?id=5953 (元のコミットメッセージに記載されているリンク)
- Go CL 12117043: https://golang.org/cl/12117043 (元のコミットメッセージに記載されているGoのコードレビューシステムへのリンク)
参考にした情報源リンク
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFkvAseX83rUP9XA37vORxX4oY-Zajr0RAqKV_R4Gku6WG4oeUibx75j1WZ3-RrJayAUfUhp-jO3wQvk6rN7VSAXUWSme_HBjttgIrZQEJo54wycjI29MaR76hUYvsI1VUeNYHwe0ei93vCLQoO1ULEmzJfXwxOvWh3cLkjDDT4EPWiAIH0IZBgvTF20XuA7vsos0G1xXsqDPd6JdCZFfcp2p2zz_ollw==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEVtbIfRahqfy9x1wLvlKhVBTzMgbKsb9528hCBC9PTxKym_SziBNx6YmgPk60r9Mnx_qj06RVX3ZNBqVq1wMXqB8A8GpHK6Ec2NSUuqr-MPS_WUpczFPIoq6OpHDiQZrWpSuKEBKsrePcXtngYAq--7RaUGYoonHqca7clmGmCAhkFPb_Mhpn6kZWvsxirypH5O3ncop4=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEY6Ufju3A_sZwrCI7hB9F3g75XqwJNDcjOLIMFLxFOuieTaKtE84QxpjPzJsB4a57QmyWI5cTJ0eileeL0lHkuR3BpU3C8YLC1gvtNNTnHbmE8-4FAemsHDSiFqGQogJYEycE=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFS4roy-iP8z7ceb1DmbdWxxrbLUNFSlIcs6Knxe0IXF4dMv4LWAH0JjpwP8EK06h46rFdFadcMc5n5hcxZd027sI5w1Dp3pktiwOQVq81P9AlBSr8VCs5PRFRNgQ==