[インデックス 12120] ファイルの概要
このコミットは、Go言語のnet/textproto
パッケージにおいて、HTTPヘッダーのパース処理を改善し、非準拠なMIMEヘッダー(特にコロンの前にスペースが含まれるもの)も許容するように変更したものです。これにより、一部のウェブサーバーが送信する、厳密にはHTTP仕様に準拠していないが、主要なブラウザやcurl
が問題なく処理するヘッダーに対応できるようになります。
コミット
commit 31e94293fc3f57f58bd0dae0698f0914b3e9a9e7
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Feb 22 11:13:59 2012 +1100
net/textproto: accept bad MIME headers as browsers do
Accept certain non-compliant response headers
(in particular, when spaces preceed the colon).
All major browser and curl seem to support this,
and at least one webserver seems to send these.
*shrug*
R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/5690059
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/31e94293fc3f57f58bd0dae0698f0914b3e9a9e7
元コミット内容
net/textproto: accept bad MIME headers as browsers do
このコミットは、ブラウザがそうであるように、不正なMIMEヘッダーを受け入れるようにします。
特定の非準拠なレスポンスヘッダー(特にコロンの前にスペースがある場合)を受け入れます。
すべての主要なブラウザとcurl
はこの動作をサポートしているようで、少なくとも1つのウェブサーバーがこれらのヘッダーを送信しているようです。
変更の背景
HTTP/MIMEヘッダーの仕様(RFC 7230やRFC 9110など)では、ヘッダーフィールド名とコロン(:
)の間に空白文字を含めることは許可されていません。しかし、現実世界のシステムでは、この仕様に厳密に準拠しないヘッダーを送信するウェブサーバーが存在します。特に、ヘッダーフィールド名とコロンの間にスペースが含まれるケースが確認されています。
このような非準拠なヘッダーであっても、主要なウェブブラウザ(IE, Firefox, Chromeなど)やコマンドラインツールであるcurl
は、多くの場合、これらのヘッダーを寛容に解釈し、問題なく処理します。これは、相互運用性を高めるための「堅牢性の原則(Postel's Law)」に基づいていると考えられます。つまり、「自分が送るときは厳密に、相手から受け取るときは寛容に」という考え方です。
Go言語のnet/textproto
パッケージは、HTTPやMIMEなどのテキストベースのプロトコルを扱うための低レベルな機能を提供します。このパッケージが厳密に仕様に準拠したヘッダーのみを許容する場合、一部のウェブサーバーからのレスポンスを正しくパースできない問題が発生します。このコミットは、このような現実世界の非準拠なヘッダーにも対応できるよう、パースロジックをより寛容にすることで、Goアプリケーションの相互運用性を向上させることを目的としています。
前提知識の解説
HTTPヘッダーとMIMEヘッダー
HTTP(Hypertext Transfer Protocol)は、ウェブ上でデータを交換するためのプロトコルです。HTTPメッセージ(リクエストとレスポンス)は、ヘッダーとボディで構成されます。ヘッダーは、メッセージに関するメタデータを提供し、フィールド名: 値
の形式で記述されます。
MIME(Multipurpose Internet Mail Extensions)は、元々電子メールで様々な種類のデータを扱うために開発された標準ですが、HTTPでもコンテンツタイプ(Content-Type
)などのヘッダーでその概念が広く利用されています。
HTTPヘッダーの構文は、RFC(Request for Comments)によって厳密に定義されています。特に、ヘッダーフィールド名とコロンの間には空白文字を置かないことが規定されています。
堅牢性の原則 (Postel's Law)
「自分が送るときは厳密に、相手から受け取るときは寛容に(Be conservative in what you do, be liberal in what you accept from others.)」という原則は、ネットワークプロトコルの設計において広く知られています。これは、異なる実装間の相互運用性を高めるために重要です。このコミットは、まさにこの原則をGoのnet/textproto
パッケージに適用した例と言えます。
net/textproto
パッケージ
Go言語の標準ライブラリであるnet/textproto
パッケージは、HTTP、NNTP、SMTPなどのテキストベースのネットワークプロトコルを扱うための低レベルな機能を提供します。このパッケージは、ヘッダーの読み書き、MIMEパートの処理など、プロトコル固有の構文解析を抽象化します。
MIMEHeader
型
net/textproto
パッケージにおけるMIMEHeader
型は、MIMEヘッダーを表すマップです。キーはヘッダーフィールド名(正規化された形式)、値は文字列のスライス(同じフィールド名が複数回出現する場合に対応)です。
CanonicalMIMEHeaderKey
関数
CanonicalMIMEHeaderKey
関数は、与えられたMIMEヘッダーのキー文字列を正規化された形式に変換します。具体的には、ハイフンで区切られた単語の最初の文字を大文字にし、それ以外の文字を小文字に変換します(例: content-type
-> Content-Type
)。これにより、ヘッダーフィールド名の大文字・小文字の区別を吸収し、一貫したキーでヘッダーにアクセスできるようになります。
技術的詳細
このコミットの主要な変更は、src/pkg/net/textproto/reader.go
ファイル内のReadMIMEHeader
関数とCanonicalMIMEHeaderKey
関数の挙動にあります。
ReadMIMEHeader
関数の変更
ReadMIMEHeader
関数は、入力ストリームからMIMEヘッダーを読み込み、MIMEHeader
マップとして返します。変更前は、ヘッダーフィールド名とコロンの間にスペースが存在すると、malformed MIME header line
エラーを返していました。
変更後、この関数は以下のロジックで非準拠なヘッダーを処理します。
- ヘッダー行からコロン(
:
)の位置を検索します。 - コロンが見つからない場合は、引き続き
malformed MIME header line
エラーを返します。 - コロンが見つかった場合、コロンまでの部分をヘッダーフィールド名(
key
)として抽出します。 - 抽出した
key
にスペースが含まれている場合、strings.TrimRight(key, " ")
を使用して末尾のスペースを削除します。これにより、"SID :"
のようなヘッダーが"SID"
として扱われるようになります。 - その後、
CanonicalMIMEHeaderKey
関数を呼び出して、正規化されたキーを取得します。
この変更により、"SID : 0"
のようなヘッダーが、ReadMIMEHeader
によって"SID"
というキーで正しくパースされるようになります。
CanonicalMIMEHeaderKey
関数の変更
CanonicalMIMEHeaderKey
関数は、ヘッダーフィールド名を正規化する役割を担っています。このコミットでは、この関数に新しいロジックが追加されました。
変更前は、この関数は単にハイフンで区切られた単語の最初の文字を大文字に変換していました。
変更後、この関数は入力文字列中にスペース(' '
)が見つかった場合、そのスペースをハイフン('-'
)に変換するようになりました。そして、そのハイフンの次の文字を大文字にするようにupper
フラグをセットします。
この変更は、例えば"Audio Mode"
のようなヘッダーフィールド名が、"Audio-Mode"
という正規化された形式に変換されることを可能にします。これは、HTTPヘッダーフィールド名が通常ハイフンで単語を区切る慣習に合わせるためのものです。
コアとなるコードの変更箇所
src/pkg/net/textproto/reader.go
--- a/src/pkg/net/textproto/reader.go
+++ b/src/pkg/net/textproto/reader.go
@@ -454,10 +454,14 @@ func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) {
// Key ends at first colon; must not have spaces.
i := bytes.IndexByte(kv, ':')
- if i < 0 || bytes.IndexByte(kv[0:i], ' ') >= 0 {
+ if i < 0 {
return m, ProtocolError("malformed MIME header line: " + string(kv))
}
- key := CanonicalMIMEHeaderKey(string(kv[0:i]))
+ key := string(kv[0:i])
+ if strings.Index(key, " ") >= 0 {
+ key = strings.TrimRight(key, " ")
+ }
+ key = CanonicalMIMEHeaderKey(key)
// Skip initial spaces in value.
i++ // skip colon
@@ -503,6 +507,11 @@ MustRewrite:
a := []byte(s)
upper := true
for i, v := range a {
+ if v == ' ' {
+ a[i] = '-'
+ upper = true
+ continue
+ }
if upper && 'a' <= v && v <= 'z' {
a[i] = v + 'A' - 'a'
}
src/pkg/net/textproto/reader_test.go
--- a/src/pkg/net/textproto/reader_test.go
+++ b/src/pkg/net/textproto/reader_test.go
@@ -164,6 +164,29 @@ func TestLargeReadMIMEHeader(t *testing.T) {
}
}
+// Test that we read slightly-bogus MIME headers seen in the wild,
+// with spaces before colons, and spaces in keys.
+func TestReadMIMEHeaderNonCompliant(t *testing.T) {
+ // Invalid HTTP response header as sent by an Axis security
+ // camera: (this is handled by IE, Firefox, Chrome, curl, etc.)
+ r := reader("Foo: bar\r\n" +
+ "Content-Language: en\r\n" +
+ "SID : 0\r\n" +
+ "Audio Mode : None\r\n" +
+ "Privilege : 127\r\n\r\n")
+ m, err := r.ReadMIMEHeader()
+ want := MIMEHeader{
+ "Foo": {"bar"},
+ "Content-Language": {"en"},
+ "Sid": {"0"},
+ "Audio-Mode": {"None"},
+ "Privilege": {"127"},
+ }
+ if !reflect.DeepEqual(m, want) || err != nil {
+ t.Fatalf("ReadMIMEHeader =\n%v, %v; want:\n%v", m, err, want)
+ }
+}
+
type readResponseTest struct {
in string
inCode int
コアとなるコードの解説
reader.go
の変更点
-
ReadMIMEHeader
関数内の変更:- 変更前:
if i < 0 || bytes.IndexByte(kv[0:i], ' ') >= 0 { ... }
- これは、コロンが見つからない場合(
i < 0
)または、ヘッダーフィールド名部分(kv[0:i]
)にスペースが含まれる場合(bytes.IndexByte(kv[0:i], ' ') >= 0
)にエラーを返すロジックでした。
- これは、コロンが見つからない場合(
- 変更後:
if i < 0 { ... }
- ヘッダーフィールド名部分にスペースが含まれていても、直ちにエラーを返さなくなりました。コロンが見つからない場合のみエラーとなります。
- 新しいロジック:
key := string(kv[0:i]) if strings.Index(key, " ") >= 0 { key = strings.TrimRight(key, " ") } key = CanonicalMIMEHeaderKey(key)
- まず、コロンまでの部分を
key
として抽出します。 - もしこの
key
にスペースが含まれていれば(例:"SID "
)、strings.TrimRight
を使って末尾のスペースを削除します(例:"SID"
)。これにより、"SID : 0"
のようなヘッダーが正しく処理されます。 - 最後に、
CanonicalMIMEHeaderKey
を呼び出して、キーを正規化します。
- まず、コロンまでの部分を
- 変更前:
-
CanonicalMIMEHeaderKey
関数内の変更:- 新しいロジック:
if v == ' ' { a[i] = '-' upper = true continue }
- このループは、ヘッダーキーの各バイトを処理し、正規化された形式に変換します。
v == ' '
の条件が追加されました。これは、入力キーにスペース文字(' '
)が含まれている場合に実行されます。a[i] = '-'
: スペースをハイフン(-
)に置き換えます。upper = true
: 次の文字を大文字にするためのフラグを立てます。これは、"Audio Mode"
が"Audio-Mode"
に変換される際に、M
が大文字になるようにするためです。continue
: 現在の文字の処理をスキップし、次の文字へ進みます。
- 新しいロジック:
reader_test.go
の変更点
TestReadMIMEHeaderNonCompliant
という新しいテストケースが追加されました。- このテストは、Axisセキュリティカメラが送信するような、非準拠なHTTPレスポンスヘッダーの例を使用しています。具体的には、
"SID : 0"
(コロンの前にスペース)と"Audio Mode : None"
(キーにスペースがあり、コロンの前にスペース)のようなヘッダーが含まれています。 - このテストは、
ReadMIMEHeader
がこれらの非準拠なヘッダーを正しくパースし、期待される正規化されたMIMEHeader
マップ(例:"Sid": {"0"}
、"Audio-Mode": {"None"}
)を返すことを検証しています。 reflect.DeepEqual
を使用して、パース結果が期待値と完全に一致するかを確認しています。
これらの変更により、net/textproto
パッケージは、より多くの現実世界のHTTPヘッダーに対応できるようになり、Goアプリケーションの堅牢性と相互運用性が向上しました。
関連リンク
- Go言語の
net/textproto
パッケージのドキュメント: https://pkg.go.dev/net/textproto - RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing: https://datatracker.ietf.org/doc/html/rfc7230
- RFC 9110 - HTTP Semantics: https://datatracker.ietf.org/doc/html/rfc9110
参考にした情報源リンク
- Web search results for "HTTP header spaces before colon non-compliant browsers curl" (Google Search)
- 特に、HTTPヘッダーの仕様、非準拠なヘッダーがセキュリティ脆弱性につながる可能性、ブラウザや
curl
の寛容な挙動に関する情報が参考になりました。
- 特に、HTTPヘッダーの仕様、非準拠なヘッダーがセキュリティ脆弱性につながる可能性、ブラウザや