[インデックス 18837] ファイルの概要
このコミットは、Go言語のnet/http/cgi
パッケージにおいて、子プロセスからの不正なCGIレスポンスに対するハンドリングを改善するものです。具体的には、CGIスクリプトがRFC 3875の規定に違反するレスポンスを返した場合に、HTTP 200 OKではなくHTTP 500 Internal Server Errorを返すように修正されています。これにより、サーバーが不正なCGIレスポンスを適切に処理し、クライアントに正確なエラー情報を提供できるようになります。
コミット
net/http/cgi: serve 500, not 200, on invalid responses from child processes
Per RFC 3875 section 6 rules.
Fixes #7198
LGTM=adg
R=adg
CC=golang-codereviews
https://golang.org/cl/68960049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d53251d4aba2820eb8f788be75a1832c6f14213b
元コミット内容
commit d53251d4aba2820eb8f788be75a1832c6f14213b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Mar 11 22:55:15 2014 -0700
net/http/cgi: serve 500, not 200, on invalid responses from child processes
Per RFC 3875 section 6 rules.
Fixes #7198
LGTM=adg
R=adg
CC=golang-codereviews
https://golang.org/cl/68960049
変更の背景
この変更の背景には、CGI (Common Gateway Interface) の仕様であるRFC 3875のセクション6に準拠する必要性がありました。以前の実装では、CGI子プロセスがHTTPヘッダーを適切に送信しなかったり、Content-Type
ヘッダーが欠落していたりするような不正なレスポンスを返した場合でも、net/http/cgi
ハンドラがHTTP 200 OKステータスコードを返してしまう可能性がありました。これは、クライアント側から見ると、リクエストが成功したかのように見えてしまうため、問題の特定やデバッグを困難にしていました。
RFC 3875のセクション6では、CGIスクリプトからのレスポンスの形式が厳密に定義されており、特にヘッダーブロックの構造や必須ヘッダーの存在が規定されています。このコミットは、これらの規定に違反するレスポンスを「不正」とみなし、HTTP 500 Internal Server Errorを返すことで、サーバーがCGIスクリプトの出力の不備をクライアントに明確に伝えるように修正することを目的としています。これにより、CGIアプリケーションの堅牢性が向上し、エラーハンドリングがより正確になります。
具体的には、GoのIssue #7198で報告された問題に対応しています。この問題は、CGIスクリプトがヘッダーを全く返さなかったり、Content-Type
ヘッダーを返さなかったりした場合に、GoのCGIハンドラが誤って200 OKを返してしまうというものでした。
前提知識の解説
CGI (Common Gateway Interface)
CGIは、Webサーバーが外部プログラム(CGIスクリプト)と連携するための標準的なインターフェースです。Webサーバーはクライアントからのリクエストを受け取ると、CGIスクリプトを起動し、リクエスト情報を環境変数や標準入力としてスクリプトに渡します。CGIスクリプトは処理結果を標準出力に書き出し、Webサーバーはその出力を受け取ってクライアントにHTTPレスポンスとして返します。
CGIスクリプトの出力は、まずHTTPヘッダーブロック(例: Content-Type: text/html
)が続き、その後に空行を挟んでメッセージボディが続くという形式が一般的です。
HTTPステータスコード
HTTPステータスコードは、Webサーバーがクライアントのリクエストに対してどのような結果を返したかを示す3桁の数字です。
- 200 OK: リクエストが成功し、要求された情報がレスポンスボディに含まれていることを示します。
- 500 Internal Server Error: サーバーがリクエストを処理する際に予期せぬエラーに遭遇したことを示します。これは、サーバー側の問題、アプリケーションのバグ、または今回のようにCGIスクリプトからの不正な出力など、様々な原因で発生する可能性があります。
RFC 3875 (The Common Gateway Interface (CGI) Version 1.1)
RFC 3875は、CGIのバージョン1.1を定義する標準ドキュメントです。このドキュメントは、WebサーバーとCGIスクリプト間のインターフェース、データ交換の形式、およびエラー処理の規則を詳細に記述しています。
特に、**セクション6「Script Response」**は、CGIスクリプトがWebサーバーに返す出力の形式について規定しています。このセクションでは、CGIスクリプトの出力は、まずHTTPヘッダーフィールドのブロックで始まり、その後に空行が続き、最後にメッセージボディが続くことを明確にしています。また、Content-Type
ヘッダーは、スクリプトがメッセージボディを返す場合に必須であるとされています。
RFC 3875のセクション6における「不正なレスポンス」とは、以下のような状況を指します。
- 不正なヘッダーブロック: CGIレスポンスは、1つ以上のヘッダーフィールドと、それに続く空行で構成されるヘッダーブロックで始まる必要があります。ヘッダーフィールドの形式が不正(例:
フィールド名: 値
の構造が守られていない)であったり、ヘッダーとメッセージボディを区切る空行が欠落していたりする場合、レスポンスは不正とみなされます。 - 不正なヘッダーフィールド値:
Status
、Content-Type
、Location
などの特定のヘッダーフィールドには、定義された構文と許容される値があります。必須ヘッダーに無効な値が提供されたり、サーバーが解析できない値であったりする場合も、レスポンスは不正になります。 - 予期しない、または非準拠の出力: ヘッダーの前に任意のテキストが出力されたり、
Content-Type
ヘッダーがないのにバイナリデータが出力されたりするなど、スクリプトが予期されるHTTPレスポンス構造に従わない出力を生成した場合、サーバーはそれを不正と判断します。 - 早期終了: スクリプトが完全で構文的に正しいレスポンスを生成する前に終了した場合も、不正なレスポンスとなります。
RFCでは、このような不正なCGIスクリプトからのレスポンスをサーバーが処理する責任があると規定しており、通常はクライアントに対して適切なHTTPエラーレスポンス(例: 500 Internal Server Error)を生成します。
技術的詳細
このコミットは、src/pkg/net/http/cgi/host.go
とsrc/pkg/net/http/cgi/matryoshka_test.go
の2つのファイルに変更を加えています。
src/pkg/net/http/cgi/host.go
の変更
host.go
は、CGIハンドラの主要なロジックを実装しているファイルです。変更の核心は、CGI子プロセスからの出力ストリームを読み込み、HTTPヘッダーを解析する部分にあります。
-
ヘッダー行のカウントと空行の検出:
headerLines
という新しい変数が追加され、読み込んだヘッダー行の数をカウントします。また、sawBlankLine
というブール変数が追加され、ヘッダーブロックの終わりを示す空行が検出されたかどうかを追跡します。statusCode := 0 headerLines := 0 // 追加 sawBlankLine := false // 追加 for { line, isPrefix, err := linebody.ReadLine() // ... if len(line) == 0 { sawBlankLine = true // 空行を検出 break } headerLines++ // ヘッダー行をカウント // ... }
-
不正なヘッダーブロックのチェック: ヘッダーの読み込みループが終了した後、以下の条件で不正なヘッダーブロックをチェックします。
headerLines == 0
: CGIスクリプトがヘッダーを全く返さなかった場合。!sawBlankLine
: ヘッダーブロックの終端を示す空行がなかった場合。
これらの条件のいずれかが真であれば、
rw.WriteHeader(http.StatusInternalServerError)
を呼び出してHTTP 500ステータスコードを返し、エラーメッセージをログに出力して処理を終了します。if headerLines == 0 || !sawBlankLine { rw.WriteHeader(http.StatusInternalServerError) h.printf("cgi: no headers") return }
-
Content-Type
ヘッダーのチェック: CGIスクリプトがメッセージボディを返す場合、Content-Type
ヘッダーは必須です。この変更では、statusCode
がまだ設定されておらず(つまり、CGIスクリプトがStatus
ヘッダーを返さなかった場合)、かつContent-Type
ヘッダーが空の場合に、HTTP 500を返すように修正されています。if statusCode == 0 && headers.Get("Content-Type") == "" { rw.WriteHeader(http.StatusInternalServerError) h.printf("cgi: missing required Content-Type in headers") return }
src/pkg/net/http/cgi/matryoshka_test.go
の変更
matryoshka_test.go
は、CGIハンドラのテストケースを含むファイルです。このコミットでは、上記の変更を検証するための新しいテストケースが追加されています。
-
新しいテスト関数の追加:
Test500WithNoHeaders
: CGIスクリプトが何もヘッダーを返さずに終了する場合をテストします。Test500WithNoContentType
: CGIスクリプトがContent-Length
ヘッダーのみを返し、Content-Type
ヘッダーを返さない場合をテストします。Test500WithEmptyHeaders
: CGIスクリプトが空行のみを返し、ヘッダーを全く返さない場合をテストします。
-
want500Test
ヘルパー関数の追加: これらの新しいテストケースは、want500Test
というヘルパー関数を使用しています。この関数は、指定されたパスに対してCGIリクエストを実行し、レスポンスのHTTPステータスコードが500であることを検証します。 -
TestBeChildCGIProcess
内のCGIスクリプトの挙動定義:TestBeChildCGIProcess
は、テスト中にCGI子プロセスとして実行される部分です。新しいテストケースに対応するため、REQUEST_URI
環境変数の値に基づいて、CGIスクリプトが特定の不正なレスポンスを返すように分岐が追加されています。/immediate-disconnect
:os.Exit(0)
を呼び出して即座に終了し、何も出力しない。/no-content-type
:Content-Length
ヘッダーのみを出力し、Content-Type
ヘッダーを省略する。/empty-headers
: 空行のみを出力し、ヘッダーを全く出力しない。
これらのテストケースにより、CGIハンドラが不正なレスポンスに対して正しくHTTP 500を返すことが保証されます。
コアとなるコードの変更箇所
src/pkg/net/http/cgi/host.go
--- a/src/pkg/net/http/cgi/host.go
+++ b/src/pkg/net/http/cgi/host.go
@@ -223,6 +223,8 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
linebody := bufio.NewReaderSize(stdoutRead, 1024)
headers := make(http.Header)
statusCode := 0
+ headerLines := 0
+ sawBlankLine := false
for {
line, isPrefix, err := linebody.ReadLine()
if isPrefix {
@@ -239,8 +241,10 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return
}
if len(line) == 0 {
+ sawBlankLine = true
break
}
+ headerLines++
parts := strings.SplitN(string(line), ":", 2)
if len(parts) < 2 {
h.printf("cgi: bogus header line: %s", string(line))
@@ -266,6 +270,11 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
headers.Add(header, val)
}
}
+ if headerLines == 0 || !sawBlankLine {
+ rw.WriteHeader(http.StatusInternalServerError)
+ h.printf("cgi: no headers")
+ return
+ }
if loc := headers.Get("Location"); loc != "" {
if strings.HasPrefix(loc, "/") && h.PathLocationHandler != nil {
@@ -277,6 +286,12 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
}
+ if statusCode == 0 && headers.Get("Content-Type") == "" {
+ rw.WriteHeader(http.StatusInternalServerError)
+ h.printf("cgi: missing required Content-Type in headers")
+ return
+ }
+
if statusCode == 0 {
statusCode = http.StatusOK
}
src/pkg/net/http/cgi/matryoshka_test.go
--- a/src/pkg/net/http/cgi/matryoshka_test.go
+++ b/src/pkg/net/http/cgi/matryoshka_test.go
@@ -128,6 +128,7 @@ func TestKillChildAfterCopyError(t *testing.T) {
}
// Test that a child handler writing only headers works.
+// golang.org/issue/7196
func TestChildOnlyHeaders(t *testing.T) {
h := &Handler{
Path: os.Args[0],
@@ -143,6 +144,26 @@ func TestChildOnlyHeaders(t *testing.T) {
}
}
+// golang.org/issue/7198
+func Test500WithNoHeaders(t *testing.T) { want500Test(t, "/immediate-disconnect") }
+func Test500WithNoContentType(t *testing.T) { want500Test(t, "/no-content-type") }
+func Test500WithEmptyHeaders(t *testing.T) { want500Test(t, "/empty-headers") }
+
+func want500Test(t *testing.T, path string) {
+ h := &Handler{
+ Path: os.Args[0],
+ Root: "/test.go",
+ Args: []string{"-test.run=TestBeChildCGIProcess"},
+ }
+ expectedMap := map[string]string{
+ "_body": "",
+ }
+ replay := runCgiTest(t, h, "GET "+path+" HTTP/1.0\\nHost: example.com\\n\\n", expectedMap)
+ if replay.Code != 500 {
+ t.Errorf("Got code %d; want 500", replay.Code)
+ }
+}
+
type neverEnding byte
func (b neverEnding) Read(p []byte) (n int, err error) {
@@ -158,6 +179,16 @@ func TestBeChildCGIProcess(t *testing.T) {
// Not in a CGI environment; skipping test.
return
}
+ switch os.Getenv("REQUEST_URI") {
+ case "/immediate-disconnect":
+ os.Exit(0)
+ case "/no-content-type":
+ fmt.Printf("Content-Length: 6\\n\\nHello\\n")
+ os.Exit(0)
+ case "/empty-headers":
+ fmt.Printf("\\nHello")
+ os.Exit(0)
+ }
Serve(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("X-Test-Header", "X-Test-Value")
req.ParseForm()
コアとなるコードの解説
src/pkg/net/http/cgi/host.go
-
headerLines
とsawBlankLine
の導入: CGIスクリプトからの出力は、まずHTTPヘッダーが複数行続き、その後に空行が来て、最後にメッセージボディが続くという形式です。headerLines
は、このヘッダー部分で何行読み込んだかを追跡します。sawBlankLine
は、ヘッダーとボディを区切る重要な空行が検出されたかどうかを記録します。これらの変数は、CGIレスポンスがRFC 3875の規定に準拠しているかを判断するための状態を保持します。 -
不正なヘッダーブロックのチェック:
if headerLines == 0 || !sawBlankLine
の条件は、CGIスクリプトがヘッダーを全く返さなかった場合(headerLines == 0
)や、ヘッダーの後に空行がなかった場合(!sawBlankLine
)を検出します。RFC 3875では、ヘッダーブロックは少なくとも1つのヘッダー行とそれに続く空行で構成される必要があるため、これらのケースは不正なレスポンスとみなされます。この場合、http.StatusInternalServerError
(500) をクライアントに返し、エラーメッセージをログに出力して処理を中断します。これにより、サーバーはCGIスクリプトの出力が不完全であることを明確に示します。 -
Content-Type
ヘッダーのチェック:if statusCode == 0 && headers.Get("Content-Type") == ""
の条件は、CGIスクリプトがHTTPステータスコードを明示的に設定せず(statusCode == 0
はデフォルトの200 OKが適用される前)、かつContent-Type
ヘッダーが欠落している場合に発動します。RFC 3875では、CGIスクリプトがメッセージボディを返す場合、Content-Type
ヘッダーは必須とされています。この条件が真の場合も、http.StatusInternalServerError
(500) を返し、エラーメッセージをログに出力します。これは、CGIスクリプトが必須ヘッダーを提供しなかったことを示し、クライアントがレスポンスボディを正しく解釈できない可能性を回避します。
src/pkg/net/http/cgi/matryoshka_test.go
-
Test500WithNoHeaders
,Test500WithNoContentType
,Test500WithEmptyHeaders
: これらのテスト関数は、host.go
で追加された不正なCGIレスポンスの検出ロジックを検証するために導入されました。それぞれ、CGIスクリプトが「ヘッダーなしで即座に終了する」「Content-Type
なしでレスポンスを返す」「空行のみを返す」という3つの異なる不正なシナリオをシミュレートします。 -
want500Test
ヘルパー関数: この関数は、上記3つのテスト関数で共通のテストロジックをカプセル化しています。CGIハンドラを設定し、特定のパスへのリクエストを実行し、その結果としてHTTP 500ステータスコードが返されることをアサートします。これにより、テストコードの重複が避けられ、可読性が向上します。 -
TestBeChildCGIProcess
内のswitch os.Getenv("REQUEST_URI")
: このswitch
文は、テスト中にCGI子プロセスとして実行される際に、REQUEST_URI
環境変数の値に基づいて異なる不正なCGIレスポンスを生成するためのものです。これにより、各テストケースが意図する不正なCGIスクリプトの挙動を正確にシミュレートできます。例えば、/immediate-disconnect
の場合は何も出力せずに終了し、/no-content-type
の場合はContent-Type
ヘッダーを省略したレスポンスを生成します。
これらの変更により、Goのnet/http/cgi
パッケージは、RFC 3875の仕様に厳密に準拠し、CGI子プロセスからの不正なレスポンスに対してより堅牢なエラーハンドリングを提供するようになりました。
関連リンク
- Go Issue #7198: https://github.com/golang/go/issues/7198
- Go CL 68960049: https://golang.org/cl/68960049
参考にした情報源リンク
- RFC 3875 - The Common Gateway Interface (CGI) Version 1.1: https://datatracker.ietf.org/doc/html/rfc3875
- RFC 3875 Section 6: Script Response (Web Search Result): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGYjKQp9nwET7Fk_GqPExqt5OdWgy5XLEDO-HmxS_-X5fevjX08UQ_pofR1srctDLNt6ULHlnIMVaV55vBJaf6LQ5bL66rcAnpsBfWAiTAEHWT-fmj5RFfYJe29kVQUU1h21rv877Z2MA==
- HTTP Status Codes (MDN Web Docs): https://developer.mozilla.org/ja/docs/Web/HTTP/Status
- Common Gateway Interface (Wikipedia): https://ja.wikipedia.org/wiki/Common_Gateway_Interface```markdown
[インデックス 18837] ファイルの概要
このコミットは、Go言語のnet/http/cgi
パッケージにおいて、子プロセスからの不正なCGIレスポンスに対するハンドリングを改善するものです。具体的には、CGIスクリプトがRFC 3875の規定に違反するレスポンスを返した場合に、HTTP 200 OKではなくHTTP 500 Internal Server Errorを返すように修正されています。これにより、サーバーが不正なCGIレスポンスを適切に処理し、クライアントに正確なエラー情報を提供できるようになります。
コミット
net/http/cgi: serve 500, not 200, on invalid responses from child processes
Per RFC 3875 section 6 rules.
Fixes #7198
LGTM=adg
R=adg
CC=golang-codereviews
https://golang.org/cl/68960049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d53251d4aba2820eb8f788be75a1832c6f14213b
元コミット内容
commit d53251d4aba2820eb8f788be75a1832c6f14213b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Mar 11 22:55:15 2014 -0700
net/http/cgi: serve 500, not 200, on invalid responses from child processes
Per RFC 3875 section 6 rules.
Fixes #7198
LGTM=adg
R=adg
CC=golang-codereviews
https://golang.org/cl/68960049
変更の背景
この変更の背景には、CGI (Common Gateway Interface) の仕様であるRFC 3875のセクション6に準拠する必要性がありました。以前の実装では、CGI子プロセスがHTTPヘッダーを適切に送信しなかったり、Content-Type
ヘッダーが欠落していたりするような不正なレスポンスを返した場合でも、net/http/cgi
ハンドラがHTTP 200 OKステータスコードを返してしまう可能性がありました。これは、クライアント側から見ると、リクエストが成功したかのように見えてしまうため、問題の特定やデバッグを困難にしていました。
RFC 3875のセクション6では、CGIスクリプトからのレスポンスの形式が厳密に定義されており、特にヘッダーブロックの構造や必須ヘッダーの存在が規定されています。このコミットは、これらの規定に違反するレスポンスを「不正」とみなし、HTTP 500 Internal Server Errorを返すことで、サーバーがCGIスクリプトの出力の不備をクライアントに明確に伝えるように修正することを目的としています。これにより、CGIアプリケーションの堅牢性が向上し、エラーハンドリングがより正確になります。
具体的には、GoのIssue #7198で報告された問題に対応しています。この問題は、CGIスクリプトがヘッダーを全く返さなかったり、Content-Type
ヘッダーを返さなかったりした場合に、GoのCGIハンドラが誤って200 OKを返してしまうというものでした。
前提知識の解説
CGI (Common Gateway Interface)
CGIは、Webサーバーが外部プログラム(CGIスクリプト)と連携するための標準的なインターフェースです。Webサーバーはクライアントからのリクエストを受け取ると、CGIスクリプトを起動し、リクエスト情報を環境変数や標準入力としてスクリプトに渡します。CGIスクリプトは処理結果を標準出力に書き出し、Webサーバーはその出力を受け取ってクライアントにHTTPレスポンスとして返します。
CGIスクリプトの出力は、まずHTTPヘッダーブロック(例: Content-Type: text/html
)が続き、その後に空行を挟んでメッセージボディが続くという形式が一般的です。
HTTPステータスコード
HTTPステータスコードは、Webサーバーがクライアントのリクエストに対してどのような結果を返したかを示す3桁の数字です。
- 200 OK: リクエストが成功し、要求された情報がレスポンスボディに含まれていることを示します。
- 500 Internal Server Error: サーバーがリクエストを処理する際に予期せぬエラーに遭遇したことを示します。これは、サーバー側の問題、アプリケーションのバグ、または今回のようにCGIスクリプトからの不正な出力など、様々な原因で発生する可能性があります。
RFC 3875 (The Common Gateway Interface (CGI) Version 1.1)
RFC 3875は、CGIのバージョン1.1を定義する標準ドキュメントです。このドキュメントは、WebサーバーとCGIスクリプト間のインターフェース、データ交換の形式、およびエラー処理の規則を詳細に記述しています。
特に、**セクション6「Script Response」**は、CGIスクリプトがWebサーバーに返す出力の形式について規定しています。このセクションでは、CGIスクリプトの出力は、まずHTTPヘッダーフィールドのブロックで始まり、その後に空行が続き、最後にメッセージボディが続くことを明確にしています。また、Content-Type
ヘッダーは、スクリプトがメッセージボディを返す場合に必須であるとされています。
RFC 3875のセクション6における「不正なレスポンス」とは、以下のような状況を指します。
- 不正なヘッダーブロック: CGIレスポンスは、1つ以上のヘッダーフィールドと、それに続く空行で構成されるヘッダーブロックで始まる必要があります。ヘッダーフィールドの形式が不正(例:
フィールド名: 値
の構造が守られていない)であったり、ヘッダーとメッセージボディを区切る空行が欠落していたりする場合、レスポンスは不正とみなされます。 - 不正なヘッダーフィールド値:
Status
、Content-Type
、Location
などの特定のヘッダーフィールドには、定義された構文と許容される値があります。必須ヘッダーに無効な値が提供されたり、サーバーが解析できない値であったりする場合も、レスポンスは不正になります。 - 予期しない、または非準拠の出力: ヘッダーの前に任意のテキストが出力されたり、
Content-Type
ヘッダーがないのにバイナリデータが出力されたりするなど、スクリプトが予期されるHTTPレスポンス構造に従わない出力を生成した場合、サーバーはそれを不正と判断します。 - 早期終了: スクリプトが完全で構文的に正しいレスポンスを生成する前に終了した場合も、不正なレスポンスとなります。
RFCでは、このような不正なCGIスクリプトからのレスポンスをサーバーが処理する責任があると規定しており、通常はクライアントに対して適切なHTTPエラーレスポンス(例: 500 Internal Server Error)を生成します。
技術的詳細
このコミットは、src/pkg/net/http/cgi/host.go
とsrc/pkg/net/http/cgi/matryoshka_test.go
の2つのファイルに変更を加えています。
src/pkg/net/http/cgi/host.go
の変更
host.go
は、CGIハンドラの主要なロジックを実装しているファイルです。変更の核心は、CGI子プロセスからの出力ストリームを読み込み、HTTPヘッダーを解析する部分にあります。
-
ヘッダー行のカウントと空行の検出:
headerLines
という新しい変数が追加され、読み込んだヘッダー行の数をカウントします。また、sawBlankLine
というブール変数が追加され、ヘッダーブロックの終わりを示す空行が検出されたかどうかを追跡します。statusCode := 0 headerLines := 0 // 追加 sawBlankLine := false // 追加 for { line, isPrefix, err := linebody.ReadLine() // ... if len(line) == 0 { sawBlankLine = true // 空行を検出 break } headerLines++ // ヘッダー行をカウント // ... }
-
不正なヘッダーブロックのチェック: ヘッダーの読み込みループが終了した後、以下の条件で不正なヘッダーブロックをチェックします。
headerLines == 0
: CGIスクリプトがヘッダーを全く返さなかった場合。!sawBlankLine
: ヘッダーブロックの終端を示す空行がなかった場合。
これらの条件のいずれかが真であれば、
rw.WriteHeader(http.StatusInternalServerError)
を呼び出してHTTP 500ステータスコードを返し、エラーメッセージをログに出力して処理を終了します。if headerLines == 0 || !sawBlankLine { rw.WriteHeader(http.StatusInternalServerError) h.printf("cgi: no headers") return }
-
Content-Type
ヘッダーのチェック: CGIスクリプトがメッセージボディを返す場合、Content-Type
ヘッダーは必須です。この変更では、statusCode
がまだ設定されておらず(つまり、CGIスクリプトがStatus
ヘッダーを返さなかった場合)、かつContent-Type
ヘッダーが空の場合に、HTTP 500を返すように修正されています。if statusCode == 0 && headers.Get("Content-Type") == "" { rw.WriteHeader(http.StatusInternalServerError) h.printf("cgi: missing required Content-Type in headers") return }
src/pkg/net/http/cgi/matryoshka_test.go
の変更
matryoshka_test.go
は、CGIハンドラのテストケースを含むファイルです。このコミットでは、上記の変更を検証するための新しいテストケースが追加されています。
-
新しいテスト関数の追加:
Test500WithNoHeaders
: CGIスクリプトが何もヘッダーを返さずに終了する場合をテストします。Test500WithNoContentType
: CGIスクリプトがContent-Length
ヘッダーのみを返し、Content-Type
ヘッダーを返さない場合をテストします。Test500WithEmptyHeaders
: CGIスクリプトが空行のみを返し、ヘッダーを全く返さない場合をテストします。
-
want500Test
ヘルパー関数の追加: これらの新しいテストケースは、want500Test
というヘルパー関数を使用しています。この関数は、指定されたパスに対してCGIリクエストを実行し、レスポンスのHTTPステータスコードが500であることを検証します。 -
TestBeChildCGIProcess
内のCGIスクリプトの挙動定義:TestBeChildCGIProcess
は、テスト中にCGI子プロセスとして実行される部分です。新しいテストケースに対応するため、REQUEST_URI
環境変数の値に基づいて、CGIスクリプトが特定の不正なレスポンスを返すように分岐が追加されています。/immediate-disconnect
:os.Exit(0)
を呼び出して即座に終了し、何も出力しない。/no-content-type
:Content-Length
ヘッダーのみを出力し、Content-Type
ヘッダーを省略する。/empty-headers
: 空行のみを出力し、ヘッダーを全く出力しない。
これらのテストケースにより、CGIハンドラが不正なレスポンスに対して正しくHTTP 500を返すことが保証されます。
コアとなるコードの変更箇所
src/pkg/net/http/cgi/host.go
--- a/src/pkg/net/http/cgi/host.go
+++ b/src/pkg/net/http/cgi/host.go
@@ -223,6 +223,8 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
linebody := bufio.NewReaderSize(stdoutRead, 1024)
headers := make(http.Header)
statusCode := 0
+ headerLines := 0
+ sawBlankLine := false
for {
line, isPrefix, err := linebody.ReadLine()
if isPrefix {
@@ -239,8 +241,10 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return
}
if len(line) == 0 {
+ sawBlankLine = true
break
}
+ headerLines++
parts := strings.SplitN(string(line), ":", 2)
if len(parts) < 2 {
h.printf("cgi: bogus header line: %s", string(line))
@@ -266,6 +270,11 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
headers.Add(header, val)
}
}
+ if headerLines == 0 || !sawBlankLine {
+ rw.WriteHeader(http.StatusInternalServerError)
+ h.printf("cgi: no headers")
+ return
+ }
if loc := headers.Get("Location"); loc != "" {
if strings.HasPrefix(loc, "/") && h.PathLocationHandler != nil {
@@ -277,6 +286,12 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
}
+ if statusCode == 0 && headers.Get("Content-Type") == "" {
+ rw.WriteHeader(http.StatusInternalServerError)
+ h.printf("cgi: missing required Content-Type in headers")
+ return
+ }
+
if statusCode == 0 {
statusCode = http.StatusOK
}
src/pkg/net/http/cgi/matryoshka_test.go
--- a/src/pkg/net/http/cgi/matryoshka_test.go
+++ b/src/pkg/net/http/cgi/matryoshka_test.go
@@ -128,6 +128,7 @@ func TestKillChildAfterCopyError(t *testing.T) {
}
// Test that a child handler writing only headers works.
+// golang.org/issue/7196
func TestChildOnlyHeaders(t *testing.T) {
h := &Handler{
Path: os.Args[0],
@@ -143,6 +144,26 @@ func TestChildOnlyHeaders(t *testing.T) {
}
}
+// golang.org/issue/7198
+func Test500WithNoHeaders(t *testing.T) { want500Test(t, "/immediate-disconnect") }
+func Test500WithNoContentType(t *testing.T) { want500Test(t, "/no-content-type") }
+func Test500WithEmptyHeaders(t *testing.T) { want500Test(t, "/empty-headers") }
+
+func want500Test(t *testing.T, path string) {
+ h := &Handler{
+ Path: os.Args[0],
+ Root: "/test.go",
+ Args: []string{"-test.run=TestBeChildCGIProcess"},
+ }
+ expectedMap := map[string]string{
+ "_body": "",
+ }
+ replay := runCgiTest(t, h, "GET "+path+" HTTP/1.0\\nHost: example.com\\n\\n", expectedMap)
+ if replay.Code != 500 {
+ t.Errorf("Got code %d; want 500", replay.Code)
+ }
+}
+
type neverEnding byte
func (b neverEnding) Read(p []byte) (n int, err error) {
@@ -158,6 +179,16 @@ func TestBeChildCGIProcess(t *testing.T) {
// Not in a CGI environment; skipping test.
return
}
+ switch os.Getenv("REQUEST_URI") {
+ case "/immediate-disconnect":
+ os.Exit(0)
+ case "/no-content-type":
+ fmt.Printf("Content-Length: 6\\n\\nHello\\n")
+ os.Exit(0)
+ case "/empty-headers":
+ fmt.Printf("\\nHello")
+ os.Exit(0)
+ }
Serve(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("X-Test-Header", "X-Test-Value")
req.ParseForm()
コアとなるコードの解説
src/pkg/net/http/cgi/host.go
-
headerLines
とsawBlankLine
の導入: CGIスクリプトからの出力は、まずHTTPヘッダーが複数行続き、その後に空行が来て、最後にメッセージボディが続くという形式です。headerLines
は、このヘッダー部分で何行読み込んだかを追跡します。sawBlankLine
は、ヘッダーとボディを区切る重要な空行が検出されたかどうかを記録します。これらの変数は、CGIレスポンスがRFC 3875の規定に準拠しているかを判断するための状態を保持します。 -
不正なヘッダーブロックのチェック:
if headerLines == 0 || !sawBlankLine
の条件は、CGIスクリプトがヘッダーを全く返さなかった場合(headerLines == 0
)や、ヘッダーの後に空行がなかった場合(!sawBlankLine
)を検出します。RFC 3875では、ヘッダーブロックは少なくとも1つのヘッダー行とそれに続く空行で構成される必要があるため、これらのケースは不正なレスポンスとみなされます。この場合、http.StatusInternalServerError
(500) をクライアントに返し、エラーメッセージをログに出力して処理を中断します。これにより、サーバーはCGIスクリプトの出力が不完全であることを明確に示します。 -
Content-Type
ヘッダーのチェック:if statusCode == 0 && headers.Get("Content-Type") == ""
の条件は、CGIスクリプトがHTTPステータスコードを明示的に設定せず(statusCode == 0
はデフォルトの200 OKが適用される前)、かつContent-Type
ヘッダーが欠落している場合に発動します。RFC 3875では、CGIスクリプトがメッセージボディを返す場合、Content-Type
ヘッダーは必須とされています。この条件が真の場合も、http.StatusInternalServerError
(500) を返し、エラーメッセージをログに出力します。これは、CGIスクリプトが必須ヘッダーを提供しなかったことを示し、クライアントがレスポンスボディを正しく解釈できない可能性を回避します。
src/pkg/net/http/cgi/matryoshka_test.go
-
Test500WithNoHeaders
,Test500WithNoContentType
,Test500WithEmptyHeaders
: これらのテスト関数は、host.go
で追加された不正なCGIレスポンスの検出ロジックを検証するために導入されました。それぞれ、CGIスクリプトが「ヘッダーなしで即座に終了する」「Content-Type
なしでレスポンスを返す」「空行のみを返す」という3つの異なる不正なシナリオをシミュレートします。 -
want500Test
ヘルパー関数: この関数は、上記3つのテスト関数で共通のテストロジックをカプセル化しています。CGIハンドラを設定し、特定のパスへのリクエストを実行し、その結果としてHTTP 500ステータスコードが返されることをアサートします。これにより、テストコードの重複が避けられ、可読性が向上します。 -
TestBeChildCGIProcess
内のswitch os.Getenv("REQUEST_URI")
: このswitch
文は、テスト中にCGI子プロセスとして実行される際に、REQUEST_URI
環境変数の値に基づいて異なる不正なCGIレスポンスを生成するためのものです。これにより、各テストケースが意図する不正なCGIスクリプトの挙動を正確にシミュレートできます。例えば、/immediate-disconnect
の場合は何も出力せずに終了し、/no-content-type
の場合はContent-Type
ヘッダーを省略したレスポンスを生成します。
これらの変更により、Goのnet/http/cgi
パッケージは、RFC 3875の仕様に厳密に準拠し、CGI子プロセスからの不正なレスポンスに対してより堅牢なエラーハンドリングを提供するようになりました。
関連リンク
- Go Issue #7198: https://github.com/golang/go/issues/7198
- Go CL 68960049: https://golang.org/cl/68960049
参考にした情報源リンク
- RFC 3875 - The Common Gateway Interface (CGI) Version 1.1: https://datatracker.ietf.org/doc/html/rfc3875
- RFC 3875 Section 6: Script Response (Web Search Result): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGYjKQp9nwET7Fk_GqPExqt5OdWgy5XLEDO-HmxS_-X5fevjX08UQ_pofR1srctDLNt6ULHlnIMVaV55vBJaf6LQ5bL66rcAnpsBfWAiTAEHWT-fmj5RFfYJe29kVQUU1h21rv877Z2MA==
- HTTP Status Codes (MDN Web Docs): https://developer.mozilla.org/ja/docs/Web/HTTP/Status
- Common Gateway Interface (Wikipedia): https://ja.wikipedia.org/wiki/Common_Gateway_Interface