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

[インデックス 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 ヘッダーは、クッキーの名前と値だけでなく、そのクッキーの振る舞いを制御する様々な属性を持ちます。

  • 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 属性の処理が以下のように変わりました。

  1. validCookieDomain による検証: c.Domain の値が validCookieDomain(c.Domain) 関数によって検証されます。
  2. 不正なドメインの破棄: validCookieDomainfalse を返した場合(つまり、Domain 属性が不正な形式である場合)、Domain 属性は Set-Cookie ヘッダーから完全に削除されます。
  3. ホストオンリークッキーへの変換: Domain 属性が削除されることで、そのクッキーは自動的にホストオンリークッキーとして扱われるようになります。これは、不正なドメインをサニタイズして誤ったドメインにクッキーが送信されるリスクを回避するための、より安全なアプローチです。
  4. ログ出力: 不正な Domain 属性が検出された場合、log.Printf を使用して警告メッセージが出力されます。これにより、開発者は不正なクッキーの生成を認識できます。

validCookieDomain(v string) bool 関数の導入

この新しい関数は、与えられた文字列 v が有効なクッキーのドメイン値であるかどうかを判断します。以下の2つの条件のいずれかを満たす場合に true を返します。

  1. isCookieDomainName(v)true を返す場合。
  2. 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 の変更点

  1. net パッケージのインポート: net.ParseIP を使用するために net パッケージがインポートされました。
  2. Cookie.String() メソッドのロジック変更:
    • if len(c.Domain) > 0 ブロック内で、sanitizeCookieDomain(c.Domain) の呼び出しが validCookieDomain(c.Domain) の呼び出しに置き換えられました。
    • validCookieDomaintrue を返した場合のみ、Domain 属性が Set-Cookie ヘッダーに追加されます。
    • validCookieDomainfalse を返した場合、log.Printf を使用して警告メッセージが出力され、Domain 属性は追加されません。これにより、クッキーはホストオンリークッキーになります。
  3. validCookieDomain 関数の追加:
    • この関数は、入力されたドメイン文字列が isCookieDomainName または有効な非IPv6 IPアドレスであるかをチェックします。
  4. isCookieDomainName 関数の追加:
    • この関数は、ドメイン名の構文規則(長さ、文字の種類、ラベルの形式、先頭のドットの扱いなど)を厳密に検証します。net パッケージの isDomainName に似ていますが、クッキーの Domain 属性の要件に合わせて調整されています。
    • 特に、s[0] == '.' のチェックにより、先頭にドットが付くドメイン名も有効と判断されます。
  5. sanitizeCookieDomain 関数の削除:
    • 以前のサニタイズロジックは削除され、不正なドメイン属性は修正されるのではなく、破棄されるようになりました。
  6. 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 関数のロジックが期待通りに機能し、セキュリティが向上したことを確認するために重要です。

関連リンク

参考にした情報源リンク