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

[インデックス 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エラーを返していました。

変更後、この関数は以下のロジックで非準拠なヘッダーを処理します。

  1. ヘッダー行からコロン(:)の位置を検索します。
  2. コロンが見つからない場合は、引き続きmalformed MIME header lineエラーを返します。
  3. コロンが見つかった場合、コロンまでの部分をヘッダーフィールド名(key)として抽出します。
  4. 抽出したkeyにスペースが含まれている場合、strings.TrimRight(key, " ")を使用して末尾のスペースを削除します。これにより、"SID :"のようなヘッダーが"SID"として扱われるようになります。
  5. その後、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の変更点

  1. 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を呼び出して、キーを正規化します。
  2. 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アプリケーションの堅牢性と相互運用性が向上しました。

関連リンク

参考にした情報源リンク

  • Web search results for "HTTP header spaces before colon non-compliant browsers curl" (Google Search)
    • 特に、HTTPヘッダーの仕様、非準拠なヘッダーがセキュリティ脆弱性につながる可能性、ブラウザやcurlの寛容な挙動に関する情報が参考になりました。