[インデックス 17168] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージにおけるクッキーの Domain
属性の取り扱いに関するセキュリティと堅牢性の改善を目的としています。具体的には、不正な形式の Domain
属性を持つクッキーが Set-Cookie
ヘッダーで送信されるのを防ぎ、代わりにそのクッキーをホストオンリークッキーとして扱うように変更しています。
コミット
commit 4f86a96ac9020f756fe4eda004ab16f2141f9746
Author: Volker Dobler <dr.volker.dobler@gmail.com>
Date: Mon Aug 12 15:14:34 2013 -0700
net/http: do not send malformed cookie domain attribute
Malformed domain attributes are not sent in a Set-Cookie header.
Instead the domain attribute is dropped which turns the cookie
into a host-only cookie. This is much safer than dropping characters
from domain attribute.
Domain attributes with a leading dot '.' are still allowed, even
if discouraged by RFC 6265 section 4.1.1.
Fixes #6013
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/12745043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4f86a96ac9020f756fe4eda004ab16f2141f9746
元コミット内容
net/http: do not send malformed cookie domain attribute
不正な形式のドメイン属性は Set-Cookie
ヘッダーで送信されません。
代わりに、ドメイン属性は削除され、クッキーはホストオンリークッキーになります。これは、ドメイン属性から文字を削除するよりもはるかに安全です。
先頭にドット .
が付くドメイン属性は、RFC 6265 のセクション 4.1.1 で推奨されていないにもかかわらず、引き続き許可されます。
Fixes #6013
変更の背景
この変更の背景には、HTTPクッキーの Domain
属性のセキュリティ上の脆弱性への対処があります。以前の実装では、不正な形式の Domain
属性が指定された場合、Goの net/http
パッケージは、その属性を「サニタイズ」(無効な文字を削除するなどして修正)しようとしていました。しかし、このようなサニタイズ処理は、意図しないドメインにクッキーが設定されてしまう可能性や、攻撃者が巧妙に細工したドメイン属性を使ってセキュリティ上の問題を引き起こす可能性をはらんでいました。
例えば、Domain=wrong;bad.abc
のような不正なドメインが指定された場合、サニタイズによって wrongbad.abc
のようなドメインとして解釈されてしまうと、開発者の意図しないドメインにクッキーが送信され、セッションハイジャックや情報漏洩のリスクが生じます。
RFC 6265 (HTTP State Management Mechanism) は、クッキーの仕様を定義しており、Domain
属性の厳密な構文と振る舞いを規定しています。このRFCに準拠しない不正な Domain
属性を安全に処理することが、このコミットの主要な動機となっています。不正なドメイン属性をサニタイズするのではなく、完全に破棄し、クッキーをホストオンリー(現在のホストにのみ有効)にすることで、セキュリティリスクを最小限に抑えるアプローチが採用されました。
また、Issue #6013 がこの問題の具体的な報告として存在し、このコミットはその問題を解決するために作成されました。
前提知識の解説
HTTP クッキー (HTTP Cookies)
HTTPクッキーは、ウェブサイトがユーザーのブラウザに保存する小さなデータ片です。主にセッション管理(ログイン状態の維持)、パーソナライゼーション(ユーザー設定の記憶)、トラッキング(ユーザー行動の追跡)などに使用されます。
クッキーは、HTTPレスポンスヘッダーの Set-Cookie
を通じてサーバーからクライアントに送信され、その後のHTTPリクエストヘッダーの Cookie
を通じてクライアントからサーバーに送り返されます。
Set-Cookie
ヘッダーと属性
Set-Cookie
ヘッダーは、クッキーの名前と値だけでなく、そのクッキーの振る舞いを制御する様々な属性を持ちます。
Domain
属性: クッキーが送信されるドメインを指定します。例えばDomain=example.com
と設定すると、example.com
およびそのサブドメイン(www.example.com
,sub.example.com
など)にクッキーが送信されます。この属性が指定されない場合、クッキーは「ホストオンリークッキー」となり、クッキーを設定したオリジンサーバーのホスト名にのみ送信されます。Path
属性: クッキーが送信されるパスを指定します。Expires
/Max-Age
属性: クッキーの有効期限を指定します。Secure
属性: HTTPS接続でのみクッキーを送信するように指定します。HttpOnly
属性: JavaScriptからクッキーにアクセスできないように指定し、XSS攻撃からの保護を強化します。
RFC 6265 (HTTP State Management Mechanism)
HTTPクッキーの現在の標準を定義しているRFCです。このRFCは、クッキーの構文、セマンティクス、セキュリティに関する詳細なガイドラインを提供しています。
- セクション 4.1.1 "The Set-Cookie Header Field":
Domain
属性を含むSet-Cookie
ヘッダーの構文と処理ルールを定義しています。特に、Domain
属性の値は有効なドメイン名である必要があり、先頭にドットが付く形式(例:.example.com
)も許可されるが、推奨されないと述べられています。 - ホストオンリークッキー:
Domain
属性が指定されない場合、クッキーは設定されたホストにのみ有効であり、そのサブドメインには送信されないという重要な概念です。
RFC 1034 (Domain Names - Concepts and Facilities) および RFC 1123 (Requirements for Internet Hosts -- Application and Support)
これらは、インターネットのドメイン名システム (DNS) の基本的な概念と、ホスト名の要件を定義するRFCです。クッキーの Domain
属性が有効なドメイン名であるかどうかを検証する際に、これらのRFCで定義されているドメイン名の構文規則が参照されます。
- ドメイン名の構文: ドメイン名は、ラベル(ピリオドで区切られた部分)で構成され、各ラベルは英数字とハイフンのみを含み、ハイフンで開始または終了してはならない、といった規則があります。
サニタイズ (Sanitization) とは
サニタイズとは、入力データから不正な文字や構造を取り除き、安全な形式に変換する処理のことです。ウェブアプリケーションにおいては、ユーザーからの入力をデータベースに保存したり、HTMLとして表示したりする前に、SQLインジェクションやクロスサイトスクリプティング (XSS) などの攻撃を防ぐために行われます。
このコミットの文脈では、不正な Domain
属性を有効なドメイン名に「修正」しようとする試みを指します。しかし、この修正が不完全であったり、攻撃者の意図を誤って解釈したりすると、かえってセキュリティ上の問題を引き起こす可能性があります。
技術的詳細
このコミットの主要な変更点は、net/http
パッケージ内の Cookie
構造体の String()
メソッドと、新しいヘルパー関数 validCookieDomain
および isCookieDomainName
の導入です。
Cookie.String()
メソッドの変更
Cookie.String()
メソッドは、Cookie
構造体の内容を Set-Cookie
ヘッダーの文字列形式に変換する役割を担っています。この変更により、Domain
属性の処理が以下のように変わりました。
validCookieDomain
による検証:c.Domain
の値がvalidCookieDomain(c.Domain)
関数によって検証されます。- 不正なドメインの破棄:
validCookieDomain
がfalse
を返した場合(つまり、Domain
属性が不正な形式である場合)、Domain
属性はSet-Cookie
ヘッダーから完全に削除されます。 - ホストオンリークッキーへの変換:
Domain
属性が削除されることで、そのクッキーは自動的にホストオンリークッキーとして扱われるようになります。これは、不正なドメインをサニタイズして誤ったドメインにクッキーが送信されるリスクを回避するための、より安全なアプローチです。 - ログ出力: 不正な
Domain
属性が検出された場合、log.Printf
を使用して警告メッセージが出力されます。これにより、開発者は不正なクッキーの生成を認識できます。
validCookieDomain(v string) bool
関数の導入
この新しい関数は、与えられた文字列 v
が有効なクッキーのドメイン値であるかどうかを判断します。以下の2つの条件のいずれかを満たす場合に true
を返します。
isCookieDomainName(v)
がtrue
を返す場合。net.ParseIP(v)
がnil
ではない(つまり、v
が有効なIPアドレスである)かつv
にコロン:
が含まれていない場合(IPv6アドレスはクッキーのドメインとして直接使用できないため)。
この関数は、ドメイン名とIPアドレスの両方を有効なクッキーのドメインとして考慮します。
isCookieDomainName(s string) bool
関数の導入
この関数は、文字列 s
が有効なドメイン名、または先頭にドット .
が付く有効なドメイン名であるかどうかをチェックします。これは、Goの net
パッケージの isDomainName
関数のロジックをベースにしていますが、クッキーの Domain
属性の特殊な要件(先頭のドットの許可)に対応するために調整されています。
主なチェック内容は以下の通りです。
- 長さの制限: ドメイン名の長さが0または255文字を超える場合は無効。
- 先頭のドットの処理:
s[0] == '.'
の場合、先頭のドットを無視して残りの部分を検証します。RFC 6265 では先頭のドットが許可されていますが、推奨はされていません。 - 文字の検証: 各文字が有効なドメイン名文字(英数字、ハイフン、ドット)であるかをチェックします。
- ハイフン
-
の前がドット.
であってはならない。 - ドット
.
の前がドット.
またはハイフン-
であってはならない。
- ハイフン
- ラベルの長さ: 各ドメイン名ラベル(ピリオドで区切られた部分)の長さが63文字を超えてはならない。
- 最終文字の検証: ドメイン名の最終文字がハイフン
-
であってはならない。 - 少なくとも1つの文字: ドメイン名に少なくとも1つの有効な文字が含まれていることを確認します。
sanitizeCookieDomain
関数の削除
以前の sanitizeCookieDomain
関数は、不正なドメイン属性をサニタイズしようとしていましたが、このコミットで削除されました。これは、サニタイズよりもドメイン属性を完全に破棄する方が安全であるという判断に基づいています。
コアとなるコードの変更箇所
src/pkg/net/http/cookie.go
--- a/src/pkg/net/http/cookie.go
+++ b/src/pkg/net/http/cookie.go
@@ -8,6 +8,7 @@ import (
"bytes"
"fmt"
"log"
+ "net"
"strconv"
"strings"
"time"
@@ -145,7 +146,15 @@ func (c *Cookie) String() string {
fmt.Fprintf(&b, "; Path=%s", sanitizeCookiePath(c.Path))
}
if len(c.Domain) > 0 {
- fmt.Fprintf(&b, "; Domain=%s", sanitizeCookieDomain(c.Domain))
+ if validCookieDomain(c.Domain) {
+ // A c.Domain containing illegal characters is not
+ // sanitized but simply dropped which turns the cookie
+ // into a host-only cookie.
+ fmt.Fprintf(&b, "; Domain=%s", c.Domain)
+ } else {
+ log.Printf("net/http: invalid Cookie.Domain %q; dropping domain attribute",
+ c.Domain)
+ }
}
if c.Expires.Unix() > 0 {
fmt.Fprintf(&b, "; Expires=%s", c.Expires.UTC().Format(time.RFC1123))
@@ -208,26 +217,78 @@ func readCookies(h Header, filter string) []*Cookie {
return cookies
}
-var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-")
+// validCookieDomain returns wheter v is a valid cookie domain-value.
+func validCookieDomain(v string) bool {
+ if isCookieDomainName(v) {
+ return true
+ }
+ if net.ParseIP(v) != nil && !strings.Contains(v, ":") {
+ return true
+ }
+ return false
+}
-// 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)
+// isCookieDomainName returns whether s is a valid domain name or a valid
+// domain name with a leading dot '.'. It is almost a direct copy of
+// package net's isDomainName.
+func isCookieDomainName(s string) bool {
+ if len(s) == 0 {
+ return false
+ }
+ if len(s) > 255 {
+ return false
+ }
+
+ if s[0] == '.' {
+ // A cookie a domain attribute may start with a leading dot.
+ s = s[1:]
+ }
+ last := byte('.')
+ ok := false // Ok once we've seen a letter.
+ partlen := 0
+ for i := 0; i < len(s); i++ {
+ c := s[i]
+ switch {
+ default:
+ return false
+ case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z':
+ // No '_' allowed here (in contrast to package net).
+ ok = true
+ partlen++
+ case '0' <= c && c <= '9':
+ // fine
+ partlen++
+ case c == '-':
+ // Byte before dash cannot be dot.
+ if last == '.' {
+ return false
+ }
+ partlen++
+ case c == '.':
+ // Byte before dot cannot be dot, dash.
+ if last == '.' || last == '-' {
+ return false
+ }
+ if partlen > 63 || partlen == 0 {
+ return false
+ }
+ partlen = 0
+ }
+ last = c
+ }
+ if last == '-' || partlen > 63 {
+ return false
+ }
+
+ return ok
}
+var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-")
+
func sanitizeCookieName(n string) string {
return cookieNameSanitizer.Replace(n)
}
-// 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
src/pkg/net/http/cookie_test.go
--- a/src/pkg/net/http/cookie_test.go
+++ b/src/pkg/net/http/cookie_test.go
@@ -32,6 +32,22 @@ var writeSetCookiesTests = []struct {
&Cookie{Name: "cookie-4", Value: "four", Path: "/restricted/"},
"cookie-4=four; Path=/restricted/",
},
+ {
+ &Cookie{Name: "cookie-5", Value: "five", Domain: "wrong;bad.abc"},
+ "cookie-5=five",
+ },
+ {
+ &Cookie{Name: "cookie-6", Value: "six", Domain: "bad-.abc"},
+ "cookie-6=six",
+ },
+ {
+ &Cookie{Name: "cookie-7", Value: "seven", Domain: "127.0.0.1"},
+ "cookie-7=seven; Domain=127.0.0.1",
+ },
+ {
+ &Cookie{Name: "cookie-8", Value: "eight", Domain: "::1"},
+ "cookie-8=eight",
+ },
}
func TestWriteSetCookies(t *testing.T) {
コアとなるコードの解説
src/pkg/net/http/cookie.go
の変更点
net
パッケージのインポート:net.ParseIP
を使用するためにnet
パッケージがインポートされました。Cookie.String()
メソッドのロジック変更:if len(c.Domain) > 0
ブロック内で、sanitizeCookieDomain(c.Domain)
の呼び出しがvalidCookieDomain(c.Domain)
の呼び出しに置き換えられました。validCookieDomain
がtrue
を返した場合のみ、Domain
属性がSet-Cookie
ヘッダーに追加されます。validCookieDomain
がfalse
を返した場合、log.Printf
を使用して警告メッセージが出力され、Domain
属性は追加されません。これにより、クッキーはホストオンリークッキーになります。
validCookieDomain
関数の追加:- この関数は、入力されたドメイン文字列が
isCookieDomainName
または有効な非IPv6 IPアドレスであるかをチェックします。
- この関数は、入力されたドメイン文字列が
isCookieDomainName
関数の追加:- この関数は、ドメイン名の構文規則(長さ、文字の種類、ラベルの形式、先頭のドットの扱いなど)を厳密に検証します。
net
パッケージのisDomainName
に似ていますが、クッキーのDomain
属性の要件に合わせて調整されています。 - 特に、
s[0] == '.'
のチェックにより、先頭にドットが付くドメイン名も有効と判断されます。
- この関数は、ドメイン名の構文規則(長さ、文字の種類、ラベルの形式、先頭のドットの扱いなど)を厳密に検証します。
sanitizeCookieDomain
関数の削除:- 以前のサニタイズロジックは削除され、不正なドメイン属性は修正されるのではなく、破棄されるようになりました。
oldCookieValueSanitizer
の削除:sanitizeCookieDomain
と共に使用されていたoldCookieValueSanitizer
も削除されました。
src/pkg/net/http/cookie_test.go
の変更点
writeSetCookiesTests
変数に新しいテストケースが追加されました。これらのテストケースは、不正な形式の Domain
属性が指定された場合に、クッキーがホストオンリークッキーとして正しく扱われることを検証します。
Domain: "wrong;bad.abc"
のような不正なドメインは、Set-Cookie
ヘッダーでDomain
属性が削除され、"cookie-5=five"
のみが出力されることを期待します。Domain: "bad-.abc"
のような不正なドメインも同様に、"cookie-6=six"
のみが出力されることを期待します。Domain: "127.0.0.1"
のような有効なIPアドレスは、Domain
属性が保持されることを期待します。Domain: "::1"
のようなIPv6アドレスは、クッキーのドメインとして直接使用できないため、Domain
属性が削除され、"cookie-8=eight"
のみが出力されることを期待します。
これらのテストケースは、新しい validCookieDomain
および isCookieDomainName
関数のロジックが期待通りに機能し、セキュリティが向上したことを確認するために重要です。
関連リンク
- Go Issue #6013: https://github.com/golang/go/issues/6013
- Go CL 12745043: https://golang.org/cl/12745043
参考にした情報源リンク
- RFC 6265 - HTTP State Management Mechanism: https://datatracker.ietf.org/doc/html/rfc6265
- RFC 1034 - Domain Names - Concepts and Facilities: https://datatracker.ietf.org/doc/html/rfc1034
- RFC 1123 - Requirements for Internet Hosts -- Application and Support: https://datatracker.ietf.org/doc/html/rfc1123
- Go
net
package documentation: https://pkg.go.dev/net - Go
net/http
package documentation: https://pkg.go.dev/net/http