[インデックス 16987] ファイルの概要
このコミットは、Go言語の net/http
パッケージにおいて、HTTPクッキーの不正な値を送信しないように修正するものです。具体的には、クッキーの名前、値、パス、ドメインに含まれる不正な文字をサニタイズ(無効な文字を除去または置換)し、RFC 6265で定義されているクッキーの仕様に準拠させることで、セキュリティと互換性を向上させています。
コミット
commit 17d803d25133b34f519cf620a28b2302677a02e8
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Aug 1 12:16:37 2013 -0700
net/http: don't write out invalid cookie lines
Fixes #3033
R=golang-dev, fvbommel, rsc
CC=golang-dev
https://golang.org/cl/12204043
---
src/pkg/net/http/cookie.go | 69 ++++++++++++++++++++++++++++++++++++-----
src/pkg/net/http/cookie_test.go | 31 ++++++++++++++++++
src/pkg/net/http/request.go | 2 +-\n 3 files changed, 94 insertions(+), 8 deletions(-)
diff --git a/src/pkg/net/http/cookie.go b/src/pkg/net/http/cookie.go
index 155b09223e..540a8f7a9a 100644
--- a/src/pkg/net/http/cookie.go
+++ b/src/pkg/net/http/cookie.go
@@ -7,6 +7,7 @@ package http
import (
"bytes"
"fmt"
+ "log"
"strconv"
"strings"
"time"
@@ -139,12 +140,12 @@ func SetCookie(w ResponseWriter, cookie *Cookie) {
// header (if other fields are set).\n func (c *Cookie) String() string {
var b bytes.Buffer
- fmt.Fprintf(&b, "%s=%s", sanitizeName(c.Name), sanitizeValue(c.Value))
+ fmt.Fprintf(&b, "%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value))
if len(c.Path) > 0 {
- fmt.Fprintf(&b, "; Path=%s", sanitizeValue(c.Path))
+ fmt.Fprintf(&b, "; Path=%s", sanitizeCookiePath(c.Path))
}
if len(c.Domain) > 0 {
- fmt.Fprintf(&b, "; Domain=%s", sanitizeValue(c.Domain))
+ fmt.Fprintf(&b, "; Domain=%s", sanitizeCookieDomain(c.Domain))
}\n if c.Expires.Unix() > 0 {
fmt.Fprintf(&b, "; Expires=%s", c.Expires.UTC().Format(time.RFC1123))
@@ -209,14 +210,68 @@ func readCookies(h Header, filter string) []*Cookie {
var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-")
-func sanitizeName(n string) string {
+// http://tools.ietf.org/html/rfc6265#section-4.1.1
+// domain-av = "Domain=" domain-value
+// domain-value = <subdomain>
+// ; defined in [RFC1034], Section 3.5, as
+// ; enhanced by [RFC1123], Section 2.1
+func sanitizeCookieDomain(v string) string {
+ // TODO: implement http://tools.ietf.org/html/rfc1034#section-3.5
+ return oldCookieValueSanitizer.Replace(v)
+}
+
+func sanitizeCookieName(n string) string {
return cookieNameSanitizer.Replace(n)
}
-var cookieValueSanitizer = strings.NewReplacer("\n", " ", "\r", " ", ";", " ")
+// This is the replacer used in the original Go cookie code.
+// It's not correct, but it's here for now until it's replaced.
+var oldCookieValueSanitizer = strings.NewReplacer("\n", " ", "\r", " ", ";", " ")
+
+// http://tools.ietf.org/html/rfc6265#section-4.1.1
+// cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
+// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
+// ; US-ASCII characters excluding CTLs,
+// ; whitespace DQUOTE, comma, semicolon,
+// ; and backslash
+func sanitizeCookieValue(v string) string {
+ return sanitizeOrWarn("Cookie.Value", validCookieValueByte, v)
+}
+
+func validCookieValueByte(b byte) bool {
+ return 0x20 < b && b < 0x7f && b != '"' && b != ',' && b != ';' && b != '\\'
+}
+
+// path-av = "Path=" path-value
+// path-value = <any CHAR except CTLs or ";">
+func sanitizeCookiePath(v string) string {
+ return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v)
+}
+
+func validCookiePathByte(b byte) bool {
+ return 0x20 <= b && b < 0x7f && b != ';'
+}
-func sanitizeValue(v string) string {
- return cookieValueSanitizer.Replace(v)
+func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string {
+ ok := true
+ for i := 0; i < len(v); i++ {
+ if valid(v[i]) {
+ continue
+ }
+ log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName)
+ ok = false
+ break
+ }
+ if ok {
+ return v
+ }
+ buf := make([]byte, 0, len(v))
+ for i := 0; i < len(v); i++ {
+ if b := v[i]; valid(b) {
+ buf = append(buf, b)
+ }
+ }
+ return string(buf)
}
func unquoteCookieValue(v string) string {
diff --git a/src/pkg/net/http/cookie_test.go b/src/pkg/net/http/cookie_test.go
index f84f73936c..0e68ff05d1 100644
--- a/src/pkg/net/http/cookie_test.go
+++ b/src/pkg/net/http/cookie_test.go
@@ -226,3 +226,34 @@ func TestReadCookies(t *testing.T) {
}
}
}\n+\n+func TestCookieSanitizeValue(t *testing.T) {
+\ttests := []struct {
+\t\tin, want string
+\t}{\n+\t\t{"foo", "foo"},
+\t\t{"foo bar", "foobar"},
+\t\t{"\x00\x7e\x7f\x80", "\x7e"},
+\t\t{`"withquotes"`, "withquotes"},
+\t}\n+\tfor _, tt := range tests {
+\t\tif got := sanitizeCookieValue(tt.in); got != tt.want {
+\t\t\tt.Errorf("sanitizeCookieValue(%q) = %q; want %q", tt.in, got, tt.want)
+\t\t}\n+\t}\n+}\n+\n+func TestCookieSanitizePath(t *testing.T) {
+\ttests := []struct {
+\t\tin, want string
+\t}{\n+\t\t{"/path", "/path"},
+\t\t{"/path with space/", "/path with space/"},
+\t\t{"/just;no;semicolon\x00orstuff/", "/justnosemicolonorstuff/"},
+\t}\n+\tfor _, tt := range tests {
+\t\tif got := sanitizeCookiePath(tt.in); got != tt.want {
+\t\t\tt.Errorf("sanitizeCookiePath(%q) = %q; want %q", tt.in, got, tt.want)
+\t\t}\n+\t}\n+}\ndiff --git a/src/pkg/net/http/request.go b/src/pkg/net/http/request.go
index 14cc42f53c..90e56225dd 100644
--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -216,7 +216,7 @@ func (r *Request) Cookie(name string) (*Cookie, error) {
// means all cookies, if any, are written into the same line,
// separated by semicolon.\n func (r *Request) AddCookie(c *Cookie) {
-\ts := fmt.Sprintf("%s=%s", sanitizeName(c.Name), sanitizeValue(c.Value))\n+\ts := fmt.Sprintf("%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value))\n \tif c := r.Header.Get("Cookie"); c != "" {
\t\tr.Header.Set("Cookie", c+"; "+s)
\t} else {
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/17d803d25133b34f519cf620a28b2302677a02e8
元コミット内容
このコミットは、Go言語の net/http
パッケージにおいて、不正なクッキー行が書き出されるのを防ぐためのものです。具体的には、クッキーの名前、値、パス、ドメインのサニタイズ処理を改善し、RFC 6265で定義されているクッキーの仕様に準拠するように変更しています。これにより、Set-Cookie
ヘッダーに不正な文字が含まれることを防ぎ、潜在的なセキュリティ問題や互換性の問題を解決します。
変更の背景
この変更は、Go issue #3033「net/http
: Set-Cookie
should sanitize values」を修正するために行われました。このイシューでは、net/http
パッケージが Set-Cookie
ヘッダーを生成する際に、クッキーの値に不正な文字(例えば、改行文字やセミコロンなど)が含まれていても、それらを適切に処理せずにそのまま出力してしまう問題が指摘されていました。
HTTPクッキーの仕様(RFC 6265)では、クッキーの名前、値、パス、ドメインに含めることができる文字が厳密に定義されています。これらのフィールドに不正な文字が含まれていると、以下のような問題が発生する可能性があります。
- セキュリティ上の脆弱性:
- HTTPレスポンス分割攻撃 (HTTP Response Splitting): クッキーの値に改行文字(
\r\n
)が含まれている場合、攻撃者はSet-Cookie
ヘッダーに続く任意のHTTPヘッダーを挿入し、レスポンスを操作できる可能性があります。これにより、セッション固定攻撃、クロスサイトスクリプティング (XSS)、キャッシュポイズニングなどの攻撃が可能になります。 - クッキーインジェクション: 不正な文字がクッキーの構文を破壊し、意図しないクッキーが設定されたり、既存のクッキーが上書きされたりする可能性があります。
- HTTPレスポンス分割攻撃 (HTTP Response Splitting): クッキーの値に改行文字(
- 互換性の問題:
- 一部のブラウザやプロキシサーバーは、RFCに準拠しないクッキーヘッダーを正しく解釈できない場合があります。これにより、アプリケーションの動作が不安定になったり、予期せぬエラーが発生したりする可能性があります。
- デバッグの困難さ:
- 不正な文字を含むクッキーは、ネットワークトラフィックの解析やデバッグを困難にする可能性があります。
これらの問題を解決するため、net/http
パッケージは、Set-Cookie
ヘッダーを生成する前に、クッキーの各フィールドをRFC 6265の仕様に従って厳密にサニタイズする必要がありました。
前提知識の解説
HTTPクッキー (HTTP Cookies)
HTTPクッキーは、ウェブサーバーがユーザーのウェブブラウザに送信する小さなデータ片です。ブラウザはこれらのクッキーを保存し、同じサーバーへの後続のリクエストでそれらを送り返します。これにより、サーバーはユーザーの状態を記憶し、セッション管理、パーソナライゼーション、トラッキングなどに利用できます。
クッキーは通常、HTTPレスポンスヘッダーの Set-Cookie
フィールドによって設定され、HTTPリクエストヘッダーの Cookie
フィールドによってサーバーに送信されます。
Set-Cookie
ヘッダーの一般的な形式は以下の通りです。
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>; Path=<path>; Domain=<domain>; Secure; HttpOnly; SameSite=<strict|lax|none>
RFC 6265: HTTP State Management Mechanism
RFC 6265は、HTTPクッキーの動作と構文を定義する標準仕様です。このRFCは、クッキーの名前、値、属性(Path
, Domain
, Expires
など)に含めることができる文字セットを厳密に規定しています。
特に重要なのは、クッキーの値 (cookie-value
) やパス (path-value
) に特定の制御文字、空白文字、区切り文字(セミコロン、カンマ、バックスラッシュ、引用符など)を含めることができないという点です。これらの文字は、クッキーの構文を破壊したり、前述のセキュリティ脆弱性を引き起こしたりする可能性があるため、厳しく制限されています。
サニタイズ (Sanitization)
サニタイズとは、入力データから不正な文字や危険な文字を除去または変換するプロセスです。ウェブアプリケーションの文脈では、ユーザーからの入力や外部から取得したデータが、アプリケーションの内部処理や出力形式の期待する形式に合致するように、安全な形式に変換することを指します。
クッキーのサニタイズにおいては、RFC 6265で許可されていない文字をクッキーのフィールドから取り除くか、安全な代替文字に置き換えることで、クッキーヘッダーの構文が常に有効であり、セキュリティ上のリスクが最小限に抑えられるようにします。
Go言語の net/http
パッケージ
Go言語の標準ライブラリである net/http
パッケージは、HTTPクライアントとサーバーの実装を提供します。このパッケージは、ウェブアプリケーションを構築するための基盤となり、HTTPリクエストの処理、レスポンスの生成、クッキーの管理など、HTTP通信に関する多くの機能を提供します。
このコミットは、net/http
パッケージが Set-Cookie
ヘッダーを生成する際の内部的なクッキー処理ロジック、特にクッキーのサニタイズ部分を改善するものです。
技術的詳細
このコミットの主要な技術的変更点は、クッキーの各フィールド(名前、値、パス、ドメイン)をRFC 6265の仕様に厳密に準拠させるための新しいサニタイズ関数群の導入と、それらを Cookie.String()
メソッドおよび Request.AddCookie()
メソッドで利用するように変更した点です。
新しいサニタイズ関数
以前は sanitizeName
と sanitizeValue
という汎用的な関数が使用されていましたが、このコミットではクッキーの各フィールドの特性に合わせたより厳密なサニタイズ関数が導入されました。
-
sanitizeCookieName(n string) string
:- クッキーの名前をサニタイズします。既存の
cookieNameSanitizer
(\n
を-
に、\r
を-
に置換) を引き続き使用します。これはRFC 6265のtoken
定義に準拠しています。
- クッキーの名前をサニタイズします。既存の
-
sanitizeCookieValue(v string) string
:- クッキーの値をサニタイズします。これは
sanitizeOrWarn
関数とvalidCookieValueByte
関数を組み合わせて実装されています。 validCookieValueByte(b byte) bool
は、RFC 6265のcookie-octet
定義に基づいて、クッキーの値に許可されるバイトをチェックします。具体的には、0x20 < b && b < 0x7f
(ASCIIのスペース以外の印字可能文字) かつb != '"' && b != ',' && b != ';' && b != '\\'
(二重引用符、カンマ、セミコロン、バックスラッシュを除く) という条件を満たすバイトのみを有効とします。
- クッキーの値をサニタイズします。これは
-
sanitizeCookiePath(v string) string
:- クッキーのパスをサニタイズします。これも
sanitizeOrWarn
関数とvalidCookiePathByte
関数を組み合わせて実装されています。 validCookiePathByte(b byte) bool
は、RFC 6265のpath-value
定義に基づいて、パスに許可されるバイトをチェックします。具体的には、0x20 <= b && b < 0x7f
(ASCIIの印字可能文字) かつb != ';'
(セミコロンを除く) という条件を満たすバイトのみを有効とします。
- クッキーのパスをサニタイズします。これも
-
sanitizeCookieDomain(v string) string
:- クッキーのドメインをサニタイズします。このコミット時点では、
oldCookieValueSanitizer
(\n
を\r
を;
を
- クッキーのドメインをサニタイズします。このコミット時点では、
sanitizeOrWarn
関数の導入
このコミットの最も重要な変更の一つは、sanitizeOrWarn
という新しいヘルパー関数の導入です。
func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string {
ok := true
for i := 0; i < len(v); i++ {
if valid(v[i]) {
continue
}
log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName)
ok = false
break
}
if ok {
return v
}
buf := make([]byte, 0, len(v))
for i := 0; i < len(v); i++ {
if b := v[i]; valid(b) {
buf = append(buf, b)
}
}
return string(buf)
}
この関数は以下のロジックで動作します。
-
初回チェックと警告:
- 入力文字列
v
をバイトごとにループし、引数で渡されたvalid
関数(例:validCookieValueByte
)を使って各バイトが有効かどうかをチェックします。 - もし無効なバイトが見つかった場合、
log.Printf
を使用して「net/http: invalid byte %q in %s; dropping invalid bytes
」という警告メッセージをログに出力します。この警告は、開発者が不正なクッキー値を設定しようとしていることを通知し、問題の特定に役立ちます。 - 無効なバイトが一つでも見つかった場合、
ok
フラグをfalse
に設定し、ループを中断します。
- 入力文字列
-
有効なバイトのみを抽出:
- もし
ok
がtrue
のまま(つまり、全てのバイトが有効だった)であれば、元の文字列v
をそのまま返します。 - もし
ok
がfalse
であれば(つまり、無効なバイトが含まれていた)、新しいバイトスライスbuf
を作成し、元の文字列v
からvalid
関数で有効と判断されたバイトのみを抽出してbuf
に追加します。 - 最終的に、
buf
から構築された新しい文字列を返します。これにより、不正なバイトが取り除かれた「サニタイズされた」文字列が生成されます。
- もし
この sanitizeOrWarn
関数の導入により、Goの net/http
パッケージは、不正なクッキー値を検出した際に単にエラーを返すのではなく、不正なバイトを削除して有効な部分のみを送信するという挙動を取るようになりました。同時に、ログに警告を出力することで、開発者が問題を認識し、修正できるように促します。これは、厳密な仕様準拠と実用性のバランスを取ったアプローチと言えます。
Cookie.String()
メソッドの変更
Cookie
構造体の String()
メソッドは、Set-Cookie
ヘッダーの値を生成するために使用されます。このメソッド内で、c.Name
, c.Value
, c.Path
, c.Domain
の各フィールドに対して、新しいサニタイズ関数が適用されるようになりました。
// 変更前
fmt.Fprintf(&b, "%s=%s", sanitizeName(c.Name), sanitizeValue(c.Value))
if len(c.Path) > 0 {
fmt.Fprintf(&b, "; Path=%s", sanitizeValue(c.Path))
}
if len(c.Domain) > 0 {
fmt.Fprintf(&b, "; Domain=%s", sanitizeValue(c.Domain))
}
// 変更後
fmt.Fprintf(&b, "%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value))
if len(c.Path) > 0 {
fmt.Fprintf(&b, "; Path=%s", sanitizeCookiePath(c.Path))
}
if len(c.Domain) > 0 {
fmt.Fprintf(&b, "; Domain=%s", sanitizeCookieDomain(c.Domain))
}
これにより、Set-Cookie
ヘッダーとして出力されるクッキーの各フィールドが、常にRFC 6265の仕様に準拠するようになります。
Request.AddCookie()
メソッドの変更
Request.AddCookie()
メソッドも同様に、クッキーの名前と値のサニタイズに新しい関数を使用するように変更されました。
// 変更前
s := fmt.Sprintf("%s=%s", sanitizeName(c.Name), sanitizeValue(c.Value))
// 変更後
s := fmt.Sprintf("%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value))
これは、クライアント側から送信される Cookie
ヘッダーを構築する際にも、適切なサニタイズが適用されることを保証します。
テストの追加
src/pkg/net/http/cookie_test.go
には、新しいサニタイズ関数 sanitizeCookieValue
と sanitizeCookiePath
の動作を検証するためのテストケースが追加されました。これにより、サニタイズロジックが期待通りに機能し、不正な文字が正しく除去されることが保証されます。
例えば、TestCookieSanitizeValue
では、改行文字 (\x00
), 制御文字 (\x7f
, \x80
), スペース、引用符などが正しく処理されるかを確認しています。
コアとなるコードの変更箇所
src/pkg/net/http/cookie.go
--- a/src/pkg/net/http/cookie.go
+++ b/src/pkg/net/http/cookie.go
@@ -7,6 +7,7 @@ package http
import (
"bytes"
"fmt"
+ "log" // 新規追加
"strconv"
"strings"
"time"
@@ -139,12 +140,12 @@ func SetCookie(w ResponseWriter, cookie *Cookie) {
// header (if other fields are set).
func (c *Cookie) String() string {
var b bytes.Buffer
- fmt.Fprintf(&b, "%s=%s", sanitizeName(c.Name), sanitizeValue(c.Value))
+ fmt.Fprintf(&b, "%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value)) // 変更
if len(c.Path) > 0 {
- fmt.Fprintf(&b, "; Path=%s", sanitizeValue(c.Path))
+ fmt.Fprintf(&b, "; Path=%s", sanitizeCookiePath(c.Path)) // 変更
}
if len(c.Domain) > 0 {
- fmt.Fprintf(&b, "; Domain=%s", sanitizeValue(c.Domain))
+ fmt.Fprintf(&b, "; Domain=%s", sanitizeCookieDomain(c.Domain)) // 変更
}
if c.Expires.Unix() > 0 {
fmt.Fprintf(&b, "; Expires=%s", c.Expires.UTC().Format(time.RFC1123))
@@ -209,14 +210,68 @@ func readCookies(h Header, filter string) []*Cookie {
var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-")
-func sanitizeName(n string) string { // 削除
+// http://tools.ietf.org/html/rfc6265#section-4.1.1
+// domain-av = "Domain=" domain-value
+// domain-value = <subdomain>
+// ; defined in [RFC1034], Section 3.5, as
+// ; enhanced by [RFC1123], Section 2.1
+func sanitizeCookieDomain(v string) string { // 新規追加
+ // TODO: implement http://tools.ietf.org/html/rfc1034#section-3.5
+ return oldCookieValueSanitizer.Replace(v)
+}
+
+func sanitizeCookieName(n string) string { // 新規追加 (旧 sanitizeName と同等だが名称変更)
return cookieNameSanitizer.Replace(n)
}
-var cookieValueSanitizer = strings.NewReplacer("\n", " ", "\r", " ", ";", " ") // 削除
+// This is the replacer used in the original Go cookie code.
+// It's not correct, but it's here for now until it's replaced.
+var oldCookieValueSanitizer = strings.NewReplacer("\n", " ", "\r", " ", ";", " ") // 新規追加 (旧 cookieValueSanitizer と同等だが名称変更)
+// http://tools.ietf.org/html/rfc6265#section-4.1.1
+// cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
+// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
+// ; US-ASCII characters excluding CTLs,
+// ; whitespace DQUOTE, comma, semicolon,
+// ; and backslash
+func sanitizeCookieValue(v string) string { // 新規追加
+ return sanitizeOrWarn("Cookie.Value", validCookieValueByte, v)
+}
+
+func validCookieValueByte(b byte) bool { // 新規追加
+ return 0x20 < b && b < 0x7f && b != '"' && b != ',' && b != ';' && b != '\\'
+}
+
+// path-av = "Path=" path-value
+// path-value = <any CHAR except CTLs or ";">
+func sanitizeCookiePath(v string) string { // 新規追加
+ return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v)
+}
+
+func validCookiePathByte(b byte) bool { // 新規追加
+ return 0x20 <= b && b < 0x7f && b != ';'
+}
-func sanitizeValue(v string) string { // 削除
- return cookieValueSanitizer.Replace(v)
+func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string { // 新規追加
+ ok := true
+ for i := 0; i < len(v); i++ {
+ if valid(v[i]) {
+ continue
+ }
+ log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName)
+ ok = false
+ break
+ }
+ if ok {
+ return v
+ }
+ buf := make([]byte, 0, len(v))
+ for i := 0; i < len(v); i++ {
+ if b := v[i]; valid(b) {
+ buf = append(buf, b)
+ }
+ }
+ return string(buf)
}
func unquoteCookieValue(v string) string {
src/pkg/net/http/cookie_test.go
--- a/src/pkg/net/http/cookie_test.go
+++ b/src/pkg/net/http/cookie_test.go
@@ -226,3 +226,34 @@ func TestReadCookies(t *testing.T) {
}
}
}\n+\n+func TestCookieSanitizeValue(t *testing.T) { // 新規追加
+\ttests := []struct {
+\t\tin, want string
+\t}{\n+\t\t{"foo", "foo"},
+\t\t{"foo bar", "foobar"},
+\t\t{"\x00\x7e\x7f\x80", "\x7e"},
+\t\t{`"withquotes"`, "withquotes"},
+\t}\n+\tfor _, tt := range tests {
+\t\tif got := sanitizeCookieValue(tt.in); got != tt.want {
+\t\t\tt.Errorf("sanitizeCookieValue(%q) = %q; want %q", tt.in, got, tt.want)
+\t\t}\n+\t}\n+}\n+\n+func TestCookieSanitizePath(t *testing.T) { // 新規追加
+\ttests := []struct {
+\t\tin, want string
+\t}{\n+\t\t{"/path", "/path"},
+\t\t{"/path with space/", "/path with space/"},
+\t\t{"/just;no;semicolon\x00orstuff/", "/justnosemicolonorstuff/"},
+\t}\n+\tfor _, tt := range tests {
+\t\tif got := sanitizeCookiePath(tt.in); got != tt.want {
+\t\t\tt.Errorf("sanitizeCookiePath(%q) = %q; want %q", tt.in, got, tt.want)
+\t\t}\n+\t}\n+}\n
src/pkg/net/http/request.go
--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -216,7 +216,7 @@ func (r *Request) Cookie(name string) (*Cookie, error) {
// means all cookies, if any, are written into the same line,
// separated by semicolon.
func (r *Request) AddCookie(c *Cookie) {
-\ts := fmt.Sprintf("%s=%s", sanitizeName(c.Name), sanitizeValue(c.Value))\n+\ts := fmt.Sprintf("%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value))\n \tif c := r.Header.Get("Cookie"); c != "" {
\t\tr.Header.Set("Cookie", c+"; "+s)
\t} else {
コアとなるコードの解説
このコミットの核となる変更は、src/pkg/net/http/cookie.go
に集約されています。
-
log
パッケージのインポート:log
パッケージが新しくインポートされています。これは、sanitizeOrWarn
関数が不正なバイトを検出した際に警告メッセージを出力するために使用されます。
-
Cookie.String()
メソッドの変更:Cookie
構造体のString()
メソッドは、HTTPレスポンスのSet-Cookie
ヘッダーに含める文字列を生成します。- 以前は
sanitizeName
とsanitizeValue
という汎用的な関数が使用されていましたが、これらがsanitizeCookieName
,sanitizeCookieValue
,sanitizeCookiePath
,sanitizeCookieDomain
という、より特化した関数に置き換えられました。これにより、各クッキーフィールドのRFC準拠が強化されます。
-
旧サニタイズ関数の削除と新サニタイズ関数の追加:
sanitizeName
とsanitizeValue
関数は削除されました。- 代わりに、RFC 6265の仕様に厳密に準拠するための新しいサニタイズ関数群が追加されました。
sanitizeCookieDomain
: クッキーのドメインをサニタイズします。現時点ではoldCookieValueSanitizer
を使用していますが、将来的な改善が示唆されています。sanitizeCookieName
: クッキーの名前をサニタイズします。既存のcookieNameSanitizer
を利用します。oldCookieValueSanitizer
: 以前のcookieValueSanitizer
と同じ機能を持つが、名称が変更されました。これは、新しいより厳密なサニタイズ関数が導入されるまでの暫定的な互換性レイヤーとして機能します。sanitizeCookieValue
: クッキーの値をサニタイズします。sanitizeOrWarn
とvalidCookieValueByte
を使用して、RFC 6265のcookie-octet
に準拠させます。validCookieValueByte
: クッキーの値に許可されるバイトを定義するヘルパー関数です。sanitizeCookiePath
: クッキーのパスをサニタイズします。sanitizeOrWarn
とvalidCookiePathByte
を使用して、RFC 6265のpath-value
に準拠させます。validCookiePathByte
: クッキーのパスに許可されるバイトを定義するヘルパー関数です。
-
sanitizeOrWarn
関数の導入:- この関数は、サニタイズ処理の共通ロジックをカプセル化します。
- 入力文字列をバイトごとに検証し、無効なバイトがあればログに警告を出力し、そのバイトを結果文字列から削除します。これにより、不正なデータが検出された場合でも、有効な部分のみでクッキーが構築され、同時に開発者に問題が通知されます。
-
src/pkg/net/http/request.go
の変更:Request.AddCookie()
メソッドも、Cookie.String()
と同様に、クッキーの名前と値のサニタイズに新しいsanitizeCookieName
とsanitizeCookieValue
関数を使用するように変更されました。これにより、クライアントがHTTPリクエストにクッキーを追加する際にも、適切なサニタイズが適用されることが保証されます。
-
src/pkg/net/http/cookie_test.go
へのテスト追加:TestCookieSanitizeValue
とTestCookieSanitizePath
という新しいテスト関数が追加されました。これらのテストは、sanitizeCookieValue
とsanitizeCookiePath
が、様々な有効および無効な入力に対して正しく動作し、RFC 6265の仕様に従って文字をサニタイズすることを確認します。特に、制御文字や特定の区切り文字が正しく除去されることを検証しています。
これらの変更により、Goの net/http
パッケージは、HTTPクッキーの生成においてより堅牢で安全な挙動を提供するようになりました。不正な文字を含むクッキーが意図せず送信されることを防ぎ、HTTPレスポンス分割攻撃などの潜在的なセキュリティ脆弱性を軽減します。
関連リンク
- Go Issue #3033: https://github.com/golang/go/issues/3033
- Go CL 12204043: https://golang.org/cl/12204043
参考にした情報源リンク
- RFC 6265 - HTTP State Management Mechanism: https://tools.ietf.org/html/rfc6265
- RFC 1034 - Domain Names - Concepts and Facilities: https://tools.ietf.org/html/rfc1034
- RFC 1123 - Requirements for Internet Hosts -- Application and Support: https://tools.ietf.org/html/rfc1123
- HTTP Response Splitting: https://owasp.org/www-community/attacks/HTTP_Response_Splitting
- HTTP Cookie: https://developer.mozilla.org/ja/docs/Web/HTTP/Cookies# [インデックス 16987] ファイルの概要
このコミットは、Go言語の net/http
パッケージにおいて、HTTPクッキーの不正な値を送信しないように修正するものです。具体的には、クッキーの名前、値、パス、ドメインに含まれる不正な文字をサニタイズ(無効な文字を除去または置換)し、RFC 6265で定義されているクッキーの仕様に準拠させることで、セキュリティと互換性を向上させています。
コミット
commit 17d803d25133b34f519cf620a28b2302677a02e8
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Aug 1 12:16:37 2013 -0700
net/http: don't write out invalid cookie lines
Fixes #3033
R=golang-dev, fvbommel, rsc
CC=golang-dev
https://golang.org/cl/12204043
---
src/pkg/net/http/cookie.go | 69 ++++++++++++++++++++++++++++++++++++-----
src/pkg/net/http/cookie_test.go | 31 ++++++++++++++++++
src/pkg/net/http/request.go | 2 +-\n 3 files changed, 94 insertions(+), 8 deletions(-)
diff --git a/src/pkg/net/http/cookie.go b/src/pkg/net/http/cookie.go
index 155b09223e..540a8f7a9a 100644
--- a/src/pkg/net/http/cookie.go
+++ b/src/pkg/net/http/cookie.go
@@ -7,6 +7,7 @@ package http
import (
"bytes"
"fmt"
+ "log"
"strconv"
"strings"
"time"
@@ -139,12 +140,12 @@ func SetCookie(w ResponseWriter, cookie *Cookie) {
// header (if other fields are set).\n func (c *Cookie) String() string {
var b bytes.Buffer
- fmt.Fprintf(&b, "%s=%s", sanitizeName(c.Name), sanitizeValue(c.Value))
+ fmt.Fprintf(&b, "%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value))
if len(c.Path) > 0 {
- fmt.Fprintf(&b, "; Path=%s", sanitizeValue(c.Path))
+ fmt.Fprintf(&b, "; Path=%s", sanitizeCookiePath(c.Path))
}
if len(c.Domain) > 0 {
- fmt.Fprintf(&b, "; Domain=%s", sanitizeValue(c.Domain))
+ fmt.Fprintf(&b, "; Domain=%s", sanitizeCookieDomain(c.Domain))
}\n if c.Expires.Unix() > 0 {
fmt.Fprintf(&b, "; Expires=%s", c.Expires.UTC().Format(time.RFC1123))
@@ -209,14 +210,68 @@ func readCookies(h Header, filter string) []*Cookie {
var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-")
-func sanitizeName(n string) string {
+// http://tools.ietf.org/html/rfc6265#section-4.1.1
+// domain-av = "Domain=" domain-value
+// domain-value = <subdomain>
+// ; defined in [RFC1034], Section 3.5, as
+// ; enhanced by [RFC1123], Section 2.1
+func sanitizeCookieDomain(v string) string {
+ // TODO: implement http://tools.ietf.org/html/rfc1034#section-3.5
+ return oldCookieValueSanitizer.Replace(v)
+}
+
+func sanitizeCookieName(n string) string {
return cookieNameSanitizer.Replace(n)
}
-var cookieValueSanitizer = strings.NewReplacer("\n", " ", "\r", " ", ";", " ")
+// This is the replacer used in the original Go cookie code.
+// It's not correct, but it's here for now until it's replaced.
+var oldCookieValueSanitizer = strings.NewReplacer("\n", " ", "\r", " ", ";", " ")
+// http://tools.ietf.org/html/rfc6265#section-4.1.1
+// cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
+// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
+// ; US-ASCII characters excluding CTLs,
+// ; whitespace DQUOTE, comma, semicolon,
+// ; and backslash
+func sanitizeCookieValue(v string) string {
+ return sanitizeOrWarn("Cookie.Value", validCookieValueByte, v)
+}
+
+func validCookieValueByte(b byte) bool {
+ return 0x20 < b && b < 0x7f && b != '"' && b != ',' && b != ';' && b != '\\'
+}
+
+// path-av = "Path=" path-value
+// path-value = <any CHAR except CTLs or ";">
+func sanitizeCookiePath(v string) string {
+ return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v)
+}
+
+func validCookiePathByte(b byte) bool {
+ return 0x20 <= b && b < 0x7f && b != ';'
+}
-func sanitizeValue(v string) string {
- return cookieValueSanitizer.Replace(v)
+func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string {
+ ok := true
+ for i := 0; i < len(v); i++ {
+ if valid(v[i]) {
+ continue
+ }
+ log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName)
+ ok = false
+ break
+ }
+ if ok {
+ return v
+ }
+ buf := make([]byte, 0, len(v))
+ for i := 0; i < len(v); i++ {
+ if b := v[i]; valid(b) {
+ buf = append(buf, b)
+ }
+ }
+ return string(buf)
}
func unquoteCookieValue(v string) string {
diff --git a/src/pkg/net/http/cookie_test.go b/src/pkg/net/http/cookie_test.go
index f84f739396c..0e68ff05d1 100644
--- a/src/pkg/net/http/cookie_test.go
+++ b/src/pkg/net/http/cookie_test.go
@@ -226,3 +226,34 @@ func TestReadCookies(t *testing.T) {
}
}
}\n+\n+func TestCookieSanitizeValue(t *testing.T) {
+\ttests := []struct {
+\t\tin, want string
+\t}{\n+\t\t{"foo", "foo"},
+\t\t{"foo bar", "foobar"},
+\t\t{"\x00\x7e\x7f\x80", "\x7e"},
+\t\t{`"withquotes"`, "withquotes"},
+\t}\n+\tfor _, tt := range tests {
+\t\tif got := sanitizeCookieValue(tt.in); got != tt.want {
+\t\t\tt.Errorf("sanitizeCookieValue(%q) = %q; want %q", tt.in, got, tt.want)
+\t\t}\n+\t}\n+}\n+\n+func TestCookieSanitizePath(t *testing.T) {
+\ttests := []struct {
+\t\tin, want string
+\t}{\n+\t\t{"/path", "/path"},
+\t\t{"/path with space/", "/path with space/"},
+\t\t{"/just;no;semicolon\x00orstuff/", "/justnosemicolonorstuff/"},
+\t}\n+\tfor _, tt := range tests {
+\t\tif got := sanitizeCookiePath(tt.in); got != tt.want {
+\t\t\tt.Errorf("sanitizeCookiePath(%q) = %q; want %q", tt.in, got, tt.want)
+\t\t}\n+\t}\n+}\n
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/17d803d25133b34f519cf620a28b2302677a02e8
元コミット内容
このコミットは、Go言語の net/http
パッケージにおいて、不正なクッキー行が書き出されるのを防ぐためのものです。具体的には、クッキーの名前、値、パス、ドメインのサニタイズ処理を改善し、RFC 6265で定義されているクッキーの仕様に準拠するように変更しています。これにより、Set-Cookie
ヘッダーに不正な文字が含まれることを防ぎ、潜在的なセキュリティ問題や互換性の問題を解決します。
変更の背景
この変更は、Go issue #3033「net/http
: Set-Cookie
should sanitize values」を修正するために行われました。このイシューでは、net/http
パッケージが Set-Cookie
ヘッダーを生成する際に、クッキーの値に不正な文字(例えば、改行文字やセミコロンなど)が含まれていても、それらを適切に処理せずにそのまま出力してしまう問題が指摘されていました。
HTTPクッキーの仕様(RFC 6265)では、クッキーの名前、値、パス、ドメインに含めることができる文字が厳密に定義されています。これらのフィールドに不正な文字が含まれていると、以下のような問題が発生する可能性があります。
- セキュリティ上の脆弱性:
- HTTPレスポンス分割攻撃 (HTTP Response Splitting): クッキーの値に改行文字(
\r\n
)が含まれている場合、攻撃者はSet-Cookie
ヘッダーに続く任意のHTTPヘッダーを挿入し、レスポンスを操作できる可能性があります。これにより、セッション固定攻撃、クロスサイトスクリプティング (XSS)、キャッシュポイズニングなどの攻撃が可能になります。 - クッキーインジェクション: 不正な文字がクッキーの構文を破壊し、意図しないクッキーが設定されたり、既存のクッキーが上書きされたりする可能性があります。
- HTTPレスポンス分割攻撃 (HTTP Response Splitting): クッキーの値に改行文字(
- 互換性の問題:
- 一部のブラウザやプロキシサーバーは、RFCに準拠しないクッキーヘッダーを正しく解釈できない場合があります。これにより、アプリケーションの動作が不安定になったり、予期せぬエラーが発生したりする可能性があります。
- デバッグの困難さ:
- 不正な文字を含むクッキーは、ネットワークトラフィックの解析やデバッグを困難にする可能性があります。
これらの問題を解決するため、net/http
パッケージは、Set-Cookie
ヘッダーを生成する前に、クッキーの各フィールドをRFC 6265の仕様に従って厳密にサニタイズする必要がありました。
前提知識の解説
HTTPクッキー (HTTP Cookies)
HTTPクッキーは、ウェブサーバーがユーザーのウェブブラウザに送信する小さなデータ片です。ブラウザはこれらのクッキーを保存し、同じサーバーへの後続のリクエストでそれらを送り返します。これにより、サーバーはユーザーの状態を記憶し、セッション管理、パーソナライゼーション、トラッキングなどに利用できます。
クッキーは通常、HTTPレスポンスヘッダーの Set-Cookie
フィールドによって設定され、HTTPリクエストヘッダーの Cookie
フィールドによってサーバーに送信されます。
Set-Cookie
ヘッダーの一般的な形式は以下の通りです。
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>; Path=<path>; Domain=<domain>; Secure; HttpOnly; SameSite=<strict|lax|none>
RFC 6265: HTTP State Management Mechanism
RFC 6265は、HTTPクッキーの動作と構文を定義する標準仕様です。このRFCは、クッキーの名前、値、属性(Path
, Domain
, Expires
など)に含めることができる文字セットを厳密に規定しています。
特に重要なのは、クッキーの値 (cookie-value
) やパス (path-value
) に特定の制御文字、空白文字、区切り文字(セミコロン、カンマ、バックスラッシュ、引用符など)を含めることができないという点です。これらの文字は、クッキーの構文を破壊したり、前述のセキュリティ脆弱性を引き起こしたりする可能性があるため、厳しく制限されています。
サニタイズ (Sanitization)
サニタイズとは、入力データから不正な文字や危険な文字を除去または変換するプロセスです。ウェブアプリケーションの文脈では、ユーザーからの入力や外部から取得したデータが、アプリケーションの内部処理や出力形式の期待する形式に合致するように、安全な形式に変換することを指します。
クッキーのサニタイズにおいては、RFC 6265で許可されていない文字をクッキーのフィールドから取り除くか、安全な代替文字に置き換えることで、クッキーヘッダーの構文が常に有効であり、セキュリティ上のリスクが最小限に抑えられるようにします。
Go言語の net/http
パッケージ
Go言語の標準ライブラリである net/http
パッケージは、HTTPクライアントとサーバーの実装を提供します。このパッケージは、ウェブアプリケーションを構築するための基盤となり、HTTPリクエストの処理、レスポンスの生成、クッキーの管理など、HTTP通信に関する多くの機能を提供します。
このコミットは、net/http
パッケージが Set-Cookie
ヘッダーを生成する際の内部的なクッキー処理ロジック、特にクッキーのサニタイズ部分を改善するものです。
技術的詳細
このコミットの主要な技術的変更点は、クッキーの各フィールド(名前、値、パス、ドメイン)をRFC 6265の仕様に厳密に準拠させるための新しいサニタイズ関数群の導入と、それらを Cookie.String()
メソッドおよび Request.AddCookie()
メソッドで利用するように変更した点です。
新しいサニタイズ関数
以前は sanitizeName
と sanitizeValue
という汎用的な関数が使用されていましたが、このコミットではクッキーの各フィールドの特性に合わせたより厳密なサニタイズ関数が導入されました。
-
sanitizeCookieName(n string) string
:- クッキーの名前をサニタイズします。既存の
cookieNameSanitizer
(\n
を-
に、\r
を-
に置換) を引き続き使用します。これはRFC 6265のtoken
定義に準拠しています。
- クッキーの名前をサニタイズします。既存の
-
sanitizeCookieValue(v string) string
:- クッキーの値をサニタイズします。これは
sanitizeOrWarn
関数とvalidCookieValueByte
関数を組み合わせて実装されています。 validCookieValueByte(b byte) bool
は、RFC 6265のcookie-octet
定義に基づいて、クッキーの値に許可されるバイトをチェックします。具体的には、0x20 < b && b < 0x7f
(ASCIIのスペース以外の印字可能文字) かつb != '"' && b != ',' && b != ';' && b != '\\'
(二重引用符、カンマ、セミコロン、バックスラッシュを除く) という条件を満たすバイトのみを有効とします。
- クッキーの値をサニタイズします。これは
-
sanitizeCookiePath(v string) string
:- クッキーのパスをサニタイズします。これも
sanitizeOrWarn
関数とvalidCookiePathByte
関数を組み合わせて実装されています。 validCookiePathByte(b byte) bool
は、RFC 6265のpath-value
定義に基づいて、パスに許可されるバイトをチェックします。具体的には、0x20 <= b && b < 0x7f
(ASCIIの印字可能文字) かつb != ';'
(セミコロンを除く) という条件を満たすバイトのみを有効とします。
- クッキーのパスをサニタイズします。これも
-
sanitizeCookieDomain(v string) string
:- クッキーのドメインをサニタイズします。このコミット時点では、
oldCookieValueSanitizer
(\n
を\r
を;
を
- クッキーのドメインをサニタイズします。このコミット時点では、
sanitizeOrWarn
関数の導入
このコミットの最も重要な変更の一つは、sanitizeOrWarn
という新しいヘルパー関数の導入です。
func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string {
ok := true
for i := 0; i < len(v); i++ {
if valid(v[i]) {
continue
}
log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName)
ok = false
break
}
if ok {
return v
}
buf := make([]byte, 0, len(v))
for i := 0; i < len(v); i++ {
if b := v[i]; valid(b) {
buf = append(buf, b)
}
}
return string(buf)
}
この関数は以下のロジックで動作します。
-
初回チェックと警告:
- 入力文字列
v
をバイトごとにループし、引数で渡されたvalid
関数(例:validCookieValueByte
)を使って各バイトが有効かどうかをチェックします。 - もし無効なバイトが見つかった場合、
log.Printf
を使用して「net/http: invalid byte %q in %s; dropping invalid bytes
」という警告メッセージをログに出力します。この警告は、開発者が不正なクッキー値を設定しようとしていることを通知し、問題の特定に役立ちます。 - 無効なバイトが一つでも見つかった場合、
ok
フラグをfalse
に設定し、ループを中断します。
- 入力文字列
-
有効なバイトのみを抽出:
- もし
ok
がtrue
のまま(つまり、全てのバイトが有効だった)であれば、元の文字列v
をそのまま返します。 - もし
ok
がfalse
であれば(つまり、無効なバイトが含まれていた)、新しいバイトスライスbuf
を作成し、元の文字列v
からvalid
関数で有効と判断されたバイトのみを抽出してbuf
に追加します。 - 最終的に、
buf
から構築された新しい文字列を返します。これにより、不正なバイトが取り除かれた「サニタイズされた」文字列が生成されます。
- もし
この sanitizeOrWarn
関数の導入により、Goの net/http
パッケージは、不正なクッキー値を検出した際に単にエラーを返すのではなく、不正なバイトを削除して有効な部分のみを送信するという挙動を取るようになりました。同時に、ログに警告を出力することで、開発者が問題を認識し、修正できるように促します。これは、厳密な仕様準拠と実用性のバランスを取ったアプローチと言えます。
Cookie.String()
メソッドの変更
Cookie
構造体の String()
メソッドは、Set-Cookie
ヘッダーの値を生成するために使用されます。このメソッド内で、c.Name
, c.Value
, c.Path
, c.Domain
の各フィールドに対して、新しいサニタイズ関数が適用されるようになりました。
// 変更前
fmt.Fprintf(&b, "%s=%s", sanitizeName(c.Name), sanitizeValue(c.Value))
if len(c.Path) > 0 {
fmt.Fprintf(&b, "; Path=%s", sanitizeValue(c.Path))
}
if len(c.Domain) > 0 {
fmt.Fprintf(&b, "; Domain=%s", sanitizeValue(c.Domain))
}
// 変更後
fmt.Fprintf(&b, "%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value))
if len(c.Path) > 0 {
fmt.Fprintf(&b, "; Path=%s", sanitizeCookiePath(c.Path))
}
if len(c.Domain) > 0 {
fmt.Fprintf(&b, "; Domain=%s", sanitizeCookieDomain(c.Domain))
}
これにより、Set-Cookie
ヘッダーとして出力されるクッキーの各フィールドが、常にRFC 6265の仕様に準拠するようになります。
Request.AddCookie()
メソッドの変更
Request.AddCookie()
メソッドも同様に、クッキーの名前と値のサニタイズに新しい関数を使用するように変更されました。
// 変更前
s := fmt.Sprintf("%s=%s", sanitizeName(c.Name), sanitizeValue(c.Value))
// 変更後
s := fmt.Sprintf("%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value))
これは、クライアント側から送信される Cookie
ヘッダーを構築する際にも、適切なサニタイズが適用されることを保証します。
テストの追加
src/pkg/net/http/cookie_test.go
には、新しいサニタイズ関数 sanitizeCookieValue
と sanitizeCookiePath
の動作を検証するためのテストケースが追加されました。これにより、サニタイズロジックが期待通りに機能し、不正な文字が正しく除去されることが保証されます。
例えば、TestCookieSanitizeValue
では、改行文字 (\x00
), 制御文字 (\x7f
, \x80
), スペース、引用符などが正しく処理されるかを確認しています。
コアとなるコードの変更箇所
src/pkg/net/http/cookie.go
--- a/src/pkg/net/http/cookie.go
+++ b/src/pkg/net/http/cookie.go
@@ -7,6 +7,7 @@ package http
import (
"bytes"
"fmt"
+ "log" // 新規追加
"strconv"
"strings"
"time"
@@ -139,12 +140,12 @@ func SetCookie(w ResponseWriter, cookie *Cookie) {
// header (if other fields are set).\n func (c *Cookie) String() string {
var b bytes.Buffer
- fmt.Fprintf(&b, "%s=%s", sanitizeName(c.Name), sanitizeValue(c.Value))
+ fmt.Fprintf(&b, "%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value)) // 変更
if len(c.Path) > 0 {
- fmt.Fprintf(&b, "; Path=%s", sanitizeValue(c.Path))
+ fmt.Fprintf(&b, "; Path=%s", sanitizeCookiePath(c.Path)) // 変更
}
if len(c.Domain) > 0 {
- fmt.Fprintf(&b, "; Domain=%s", sanitizeValue(c.Domain))
+ fmt.Fprintf(&b, "; Domain=%s", sanitizeCookieDomain(c.Domain)) // 変更
}
if c.Expires.Unix() > 0 {
fmt.Fprintf(&b, "; Expires=%s", c.Expires.UTC().Format(time.RFC1123))
@@ -209,14 +210,68 @@ func readCookies(h Header, filter string) []*Cookie {
var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-")
-func sanitizeName(n string) string { // 削除
+// http://tools.ietf.org/html/rfc6265#section-4.1.1
+// domain-av = "Domain=" domain-value
+// domain-value = <subdomain>
+// ; defined in [RFC1034], Section 3.5, as
+// ; enhanced by [RFC1123], Section 2.1
+func sanitizeCookieDomain(v string) string { // 新規追加
+ // TODO: implement http://tools.ietf.org/html/rfc1034#section-3.5
+ return oldCookieValueSanitizer.Replace(v)
+}
+
+func sanitizeCookieName(n string) string { // 新規追加 (旧 sanitizeName と同等だが名称変更)
return cookieNameSanitizer.Replace(n)
}
-var cookieValueSanitizer = strings.NewReplacer("\n", " ", "\r", " ", ";", " ") // 削除
+// This is the replacer used in the original Go cookie code.
+// It's not correct, but it's here for now until it's replaced.
+var oldCookieValueSanitizer = strings.NewReplacer("\n", " ", "\r", " ", ";", " ") // 新規追加 (旧 cookieValueSanitizer と同等だが名称変更)
+// http://tools.ietf.org/html/rfc6265#section-4.1.1
+// cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
+// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
+// ; US-ASCII characters excluding CTLs,
+// ; whitespace DQUOTE, comma, semicolon,
+// ; and backslash
+func sanitizeCookieValue(v string) string { // 新規追加
+ return sanitizeOrWarn("Cookie.Value", validCookieValueByte, v)
+}
+
+func validCookieValueByte(b byte) bool { // 新規追加
+ return 0x20 < b && b < 0x7f && b != '"' && b != ',' && b != ';' && b != '\\'
+}
+
+// path-av = "Path=" path-value
+// path-value = <any CHAR except CTLs or ";">
+func sanitizeCookiePath(v string) string { // 新規追加
+ return sanitizeOrWarn("Cookie.Path", validCookiePathByte, v)
+}
+
+func validCookiePathByte(b byte) bool { // 新規追加
+ return 0x20 <= b && b < 0x7f && b != ';'
+}
-func sanitizeValue(v string) string { // 削除
- return cookieValueSanitizer.Replace(v)
+func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string { // 新規追加
+ ok := true
+ for i := 0; i < len(v); i++ {
+ if valid(v[i]) {
+ continue
+ }
+ log.Printf("net/http: invalid byte %q in %s; dropping invalid bytes", v[i], fieldName)
+ ok = false
+ break
+ }
+ if ok {
+ return v
+ }
+ buf := make([]byte, 0, len(v))
+ for i := 0; i < len(v); i++ {
+ if b := v[i]; valid(b) {
+ buf = append(buf, b)
+ }
+ }
+ return string(buf)
}
func unquoteCookieValue(v string) string {
src/pkg/net/http/cookie_test.go
--- a/src/pkg/net/http/cookie_test.go
+++ b/src/pkg/net/http/cookie_test.go
@@ -226,3 +226,34 @@ func TestReadCookies(t *testing.T) {
}
}
}\n+\n+func TestCookieSanitizeValue(t *testing.T) { // 新規追加
+\ttests := []struct {
+\t\tin, want string
+\t}{\n+\t\t{"foo", "foo"},
+\t\t{"foo bar", "foobar"},
+\t\t{"\x00\x7e\x7f\x80", "\x7e"},
+\t\t{`"withquotes"`, "withquotes"},
+\t}\n+\tfor _, tt := range tests {
+\t\tif got := sanitizeCookieValue(tt.in); got != tt.want {
+\t\t\tt.Errorf("sanitizeCookieValue(%q) = %q; want %q", tt.in, got, tt.want)
+\t\t}\n+\t}\n+}\n+\n+func TestCookieSanitizePath(t *testing.T) { // 新規追加
+\ttests := []struct {
+\t\tin, want string
+\t}{\n+\t\t{"/path", "/path"},
+\t\t{"/path with space/", "/path with space/"},
+\t\t{"/just;no;semicolon\x00orstuff/", "/justnosemicolonorstuff/"},
+\t}\n+\tfor _, tt := range tests {
+\t\tif got := sanitizeCookiePath(tt.in); got != tt.want {
+\t\t\tt.Errorf("sanitizeCookiePath(%q) = %q; want %q", tt.in, got, tt.want)
+\t\t}\n+\t}\n+}\n
src/pkg/net/http/request.go
--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -216,7 +216,7 @@ func (r *Request) Cookie(name string) (*Cookie, error) {
// means all cookies, if any, are written into the same line,
// separated by semicolon.
func (r *Request) AddCookie(c *Cookie) {
-\ts := fmt.Sprintf("%s=%s", sanitizeName(c.Name), sanitizeValue(c.Value))\n+\ts := fmt.Sprintf("%s=%s", sanitizeCookieName(c.Name), sanitizeCookieValue(c.Value))\n \tif c := r.Header.Get("Cookie"); c != "" {
\t\tr.Header.Set("Cookie", c+"; "+s)
\t} else {
コアとなるコードの解説
このコミットの核となる変更は、src/pkg/net/http/cookie.go
に集約されています。
-
log
パッケージのインポート:log
パッケージが新しくインポートされています。これは、sanitizeOrWarn
関数が不正なバイトを検出した際に警告メッセージを出力するために使用されます。
-
Cookie.String()
メソッドの変更:Cookie
構造体のString()
メソッドは、HTTPレスポンスのSet-Cookie
ヘッダーに含める文字列を生成します。- 以前は
sanitizeName
とsanitizeValue
という汎用的な関数が使用されていましたが、これらがsanitizeCookieName
,sanitizeCookieValue
,sanitizeCookiePath
,sanitizeCookieDomain
という、より特化した関数に置き換えられました。これにより、各クッキーフィールドのRFC準拠が強化されます。
-
旧サニタイズ関数の削除と新サニタイズ関数の追加:
sanitizeName
とsanitizeValue
関数は削除されました。- 代わりに、RFC 6265の仕様に厳密に準拠するための新しいサニタイズ関数群が追加されました。
sanitizeCookieDomain
: クッキーのドメインをサニタイズします。現時点ではoldCookieValueSanitizer
を使用していますが、将来的な改善が示唆されています。sanitizeCookieName
: クッキーの名前をサニタイズします。既存のcookieNameSanitizer
を利用します。oldCookieValueSanitizer
: 以前のcookieValueSanitizer
と同じ機能を持つが、名称が変更されました。これは、新しいより厳密なサニタイズ関数が導入されるまでの暫定的な互換性レイヤーとして機能します。sanitizeCookieValue
: クッキーの値をサニタイズします。sanitizeOrWarn
とvalidCookieValueByte
を使用して、RFC 6265のcookie-octet
に準拠させます。validCookieValueByte
: クッキーの値に許可されるバイトを定義するヘルパー関数です。sanitizeCookiePath
: クッキーのパスをサニタイズします。sanitizeOrWarn
とvalidCookiePathByte
を使用して、RFC 6265のpath-value
に準拠させます。validCookiePathByte
: クッキーのパスに許可されるバイトを定義するヘルパー関数です。
-
sanitizeOrWarn
関数の導入:- この関数は、サニタイズ処理の共通ロジックをカプセル化します。
- 入力文字列をバイトごとに検証し、無効なバイトがあればログに警告を出力し、そのバイトを結果文字列から削除します。これにより、不正なデータが検出された場合でも、有効な部分のみでクッキーが構築され、同時に開発者に問題が通知されます。
-
src/pkg/net/http/request.go
の変更:Request.AddCookie()
メソッドも、Cookie.String()
と同様に、クッキーの名前と値のサニタイズに新しいsanitizeCookieName
とsanitizeCookieValue
関数を使用するように変更されました。これにより、クライアントがHTTPリクエストにクッキーを追加する際にも、適切なサニタイズが適用されることが保証されます。
-
src/pkg/net/http/cookie_test.go
へのテスト追加:TestCookieSanitizeValue
とTestCookieSanitizePath
という新しいテスト関数が追加されました。これらのテストは、sanitizeCookieValue
とsanitizeCookiePath
が、様々な有効および無効な入力に対して正しく動作し、RFC 6265の仕様に従って文字をサニタイズすることを確認します。特に、制御文字や特定の区切り文字が正しく除去されることを検証しています。
これらの変更により、Goの net/http
パッケージは、HTTPクッキーの生成においてより堅牢で安全な挙動を提供するようになりました。不正な文字を含むクッキーが意図せず送信されることを防ぎ、HTTPレスポンス分割攻撃などの潜在的なセキュリティ脆弱性を軽減します。
関連リンク
- Go Issue #3033: https://github.com/golang/go/issues/3033
- Go CL 12204043: https://golang.org/cl/12204043
参考にした情報源リンク
- RFC 6265 - HTTP State Management Mechanism: https://tools.ietf.org/html/rfc6265
- RFC 1034 - Domain Names - Concepts and Facilities: https://tools.ietf.org/html/rfc1034
- RFC 1123 - Requirements for Internet Hosts -- Application and Support: https://tools.ietf.org/html/rfc1123
- HTTP Response Splitting: https://owasp.org/www-community/attacks/HTTP_Response_Splitting
- HTTP Cookie: https://developer.mozilla.org/ja/docs/Web/HTTP/Cookies