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

[インデックス 17138] ファイルの概要

このコミットは、Go言語の標準ライブラリであるencoding/jsonパッケージにおいて、JSON文字列をエスケープする際に、これまでエスケープされていなかった&(アンパサンド)文字を常にエスケープするように変更するものです。これにより、JSONエンコーディングの一貫性とセキュリティが向上します。

コミット

commit 080e00d55d37be67be30e2723233594e64097edf
Author: Russ Cox <rsc@golang.org>
Date:   Fri Aug 9 18:33:57 2013 -0400

    encoding/json: escape & always
    
    There are a few different places in the code that escape
    possibly-problematic characters like < > and &.
    This one was the only one missing &, so add it.
    
    This means that if you Marshal a string, you get the
    same answer you do if you Marshal a string and
    pass it through the compactor. (Ironically, the
    compaction makes the string longer.)
    
    Because html/template invokes json.Marshal to
    prepare escaped strings for JavaScript, this changes
    the form of some of the escaped strings, but not
    their meaning.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/12708044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/080e00d55d37be67be30e2723233594e64097edf

元コミット内容

encoding/json: escape & always

このコミットは、encoding/jsonパッケージがJSON文字列をエスケープする際に、&文字を常にエスケープするように変更します。コードベースには<>といった問題のある文字をエスケープする箇所がいくつか存在しますが、&だけが欠けていたため、これを追加します。

この変更により、文字列をjson.Marshalでエンコードした場合と、エンコード後にコンパクター(おそらく内部的な最適化処理)を通した場合とで、同じ結果が得られるようになります。(皮肉なことに、この変更によって文字列が長くなる場合があります。)

html/templateパッケージは、JavaScript向けにエスケープされた文字列を準備するためにjson.Marshalを呼び出すため、この変更はエスケープされた文字列の形式を変更しますが、その意味は変わりません。

変更の背景

JSON(JavaScript Object Notation)は、データ交換のための軽量なデータ形式です。Webアプリケーションにおいて、JSONデータはしばしばHTMLドキュメント内に埋め込まれたJavaScriptコード内で利用されます。この際、JSON文字列に含まれる特定の文字が、HTMLやJavaScriptの構文と衝突し、セキュリティ上の脆弱性(特にクロスサイトスクリプティング: XSS)を引き起こす可能性があります。

Goのencoding/jsonパッケージは、JSON仕様に準拠しつつ、これらのセキュリティリスクを軽減するためのエスケープ処理を行っています。しかし、これまでの実装では、HTMLの特殊文字である&(アンパサンド)がJSON文字列内でエスケープされていませんでした。

例えば、JSON文字列が<script>タグ内に直接埋め込まれる場合、&がエスケープされていないと、HTMLエンティティとして解釈され、意図しない挙動を引き起こす可能性があります。特に、&はHTMLエンティティの開始文字であるため、&amp;&#x26;のようにエスケープされるべきです。

このコミットの背景には、encoding/jsonが生成するJSON文字列が、HTMLコンテキスト、特にhtml/templateパッケージを通じてJavaScriptに埋め込まれる際に、より安全で一貫性のあるエスケープ処理を提供する必要性があったと考えられます。既存のコードベースで<>はエスケープされていたにもかかわらず、&が漏れていたという点が、この変更の直接的な動機となっています。

前提知識の解説

JSON (JavaScript Object Notation)

JSONは、人間が読み書きしやすく、機械が解析・生成しやすいデータ交換フォーマットです。JavaScriptのオブジェクトリテラルをベースにしていますが、言語に依存しないデータ形式として広く利用されています。

エスケープ処理

エスケープ処理とは、特定の文字が特別な意味を持つコンテキストにおいて、その文字を通常の文字として扱わせるために、別の表現に変換することです。例えば、JSON文字列内でダブルクォーテーション"を表現したい場合、\"のようにバックスラッシュを前置してエスケープします。

Webセキュリティの文脈では、HTMLやJavaScriptの特殊文字を適切にエスケープすることが非常に重要です。これにより、悪意のあるスクリプトの挿入(XSS攻撃)を防ぐことができます。

クロスサイトスクリプティング (XSS)

XSSは、Webアプリケーションの脆弱性の一つで、攻撃者が悪意のあるスクリプトをWebページに挿入し、そのスクリプトがユーザーのブラウザで実行されることで発生します。これにより、セッションハイジャック、個人情報の窃取、Webサイトの改ざんなどが行われる可能性があります。

JSONデータがHTMLやJavaScriptに埋め込まれる場合、JSON文字列内の特殊文字が適切にエスケープされていないと、XSSの脆弱性につながることがあります。例えば、JSON文字列内に<script>タグやイベントハンドラ属性(onclickなど)を挿入されると、それがHTMLとして解釈されてしまい、攻撃者の意図するスクリプトが実行されてしまう可能性があります。

html/templateパッケージ

Go言語のhtml/templateパッケージは、HTMLテンプレートを安全に生成するためのパッケージです。このパッケージは、テンプレートに挿入されるデータに対して自動的にエスケープ処理を施し、XSSなどの脆弱性を防ぐことを目的としています。

html/templateは、JavaScriptコンテキストにデータを挿入する際に、内部的にjson.Marshalを利用することがあります。このため、json.Marshalが生成するJSON文字列が、html/templateのセキュリティ要件を満たすように、適切なエスケープ処理を行う必要があります。

Unicodeエスケープシーケンス

JSONでは、非ASCII文字や特定の制御文字を\uXXXX形式のUnicodeエスケープシーケンスで表現することができます。例えば、&\u0026とエスケープされます。これは、文字そのものではなく、その文字のUnicodeコードポイントを16進数で表現したものです。

技術的詳細

このコミットの主要な変更点は、src/pkg/encoding/json/encode.goファイルのencodeState.stringメソッドにおける文字エスケープロジックの修正です。

変更前は、0x20(スペース)以上のASCII文字で、かつ\"<>ではない文字はそのまま出力されていました。しかし、この条件に&が含まれていなかったため、&はエスケープされずにJSON文字列にそのまま出力されていました。

変更後は、この条件に&& b != '&'が追加され、&文字もエスケープ対象となりました。これにより、&はJSONの仕様に従って\u0026としてエスケープされるようになります。

この変更は、特にhtml/templateパッケージがjson.Marshalを利用してJavaScriptコンテキストにデータを埋め込む際に重要です。html/templateは、HTMLのセキュリティを確保するために、挿入されるデータに対して厳格なエスケープ処理を行います。json.Marshal&をエスケープしない場合、html/templateが生成するHTML/JavaScriptコードに潜在的な脆弱性が残る可能性がありました。

例えば、html/templateがJSON文字列を<script>タグ内に挿入する際、&がエスケープされていないと、ブラウザがそれをHTMLエンティティの開始として解釈し、予期せぬ挙動やXSS攻撃につながる可能性があります。\u0026としてエスケープすることで、このリスクが排除され、JSON文字列がJavaScriptコードとして安全に解釈されることが保証されます。

また、コミットメッセージにある「Marshalした文字列と、compactorを通した文字列が同じになる」という点は、encoding/jsonパッケージ内部の最適化や処理フローにおける一貫性の向上を示唆しています。compactorがどのような処理を指すのかは明確ではありませんが、おそらくJSON文字列の整形や最適化を行う内部的なコンポーネントであり、そこでのエスケープルールとMarshalのエスケープルールが統一されたことを意味していると考えられます。

テストファイルの変更は、この新しいエスケープルールが正しく適用されていることを確認するためのものです。特にsrc/pkg/html/template/content_test.gosrc/pkg/html/template/escape_test.goでは、&を含む文字列が\u0026としてエスケープされていることを検証するテストケースが追加または修正されています。これにより、html/templatejson.Marshalと連携して安全な出力を生成できることが保証されます。

コアとなるコードの変更箇所

src/pkg/encoding/json/encode.go

--- a/src/pkg/encoding/json/encode.go
+++ b/src/pkg/encoding/json/encode.go
@@ -734,7 +734,7 @@ func (e *encodeState) string(s string) (int, error) {
 	start := 0
 	for i := 0; i < len(s); {
 		if b := s[i]; b < utf8.RuneSelf {
-			if 0x20 <= b && b != '\\' && b != '"' && b != '<' && b != '>' {
+			if 0x20 <= b && b != '\\' && b != '"' && b != '<' && b != '>' && b != '&' {
 				i++
 				continue
 			}

src/pkg/html/template/content_test.go

--- a/src/pkg/html/template/content_test.go
+++ b/src/pkg/html/template/content_test.go
@@ -123,29 +123,29 @@ func TestTypedContent(t *testing.T) {
 		{
 			`<script>alert({{.}})</script>`,
 			[]string{
-				`"<b> \"foo%\" O'Reilly &bar;"`,
+				`"<b> \"foo%\" O'Reilly \u0026bar;"`,
 				`"a[href =~ \"//example.com\"]#foo"`,
-				`"Hello, <b>World</b> &amp;tc!"`,
+				`"Hello, <b>World</b> \u0026amp;tc!"`,
 				`" dir=\"ltr\""`,
 				// Not escaped.
 				`c && alert("Hello, World!");`,
 				// Escape sequence not over-escaped.
 				`"Hello, World & O'Reilly\x21"`,
-				`"greeting=H%69&addressee=(World)"`,
+				`"greeting=H%69\u0026addressee=(World)"`,
 			},
 		},
 		{
 			`<button onclick="alert({{.}})">`,
 			[]string{
-				`&#34;<b> &#34;foo%&#34; O&#39;Reilly &amp;bar;&#34;`,
+				`&#34;<b> &#34;foo%&#34; O&#39;Reilly \u0026bar;&#34;`,
 				`&#34;a[href =~ &#34;//example.com&#34;]#foo&#34;`,
-				`&#34;Hello, <b>World</b> &amp;amp;tc!&#34;`,
+				`&#34;Hello, <b>World</b> \u0026amp;tc!&#34;`,
 				`&#34; dir=\&#34;ltr\&#34;&#34;`,
 				// Not JS escaped but HTML escaped.
 				`c &amp;&amp; alert(&#34;Hello, World!&#34;);`,
 				// Escape sequence not over-escaped.
 				`&#34;Hello, World &amp; O&#39;Reilly\x21&#34;`,
-				`&#34;greeting=H%69&amp;addressee=(World)&#34;`,
+				`&#34;greeting=H%69\u0026addressee=(World)&#34;`,
 			},
 		},

src/pkg/html/template/escape_test.go

--- a/src/pkg/html/template/escape_test.go
+++ b/src/pkg/html/template/escape_test.go
@@ -538,7 +538,7 @@ func TestEscape(t *testing.T) {
 		{
 			"typed HTML in script",
 			`<button onclick="alert({{.W}})">`,
-			`<button onclick="alert(&#34;&iexcl;\u003cb class=\&#34;foo\&#34;\u003eHello\u003c/b\u003e, \u003ctextarea\u003eO&#39;World\u003c/textarea\u003e!&#34;)">`,
+			`<button onclick="alert(&#34;\u0026iexcl;\u003cb class=\&#34;foo\&#34;\u003eHello\u003c/b\u003e, \u003ctextarea\u003eO&#39;World\u003c/textarea\u003e!&#34;)">`,
 		},
 		{
 			"typed HTML in RCDATA",

コアとなるコードの解説

src/pkg/encoding/json/encode.go の変更

encodeState.string(s string) メソッドは、Goの文字列sをJSON文字列としてエンコードする際の主要なロジックを含んでいます。このメソッドは、文字列をバイト列として走査し、エスケープが必要な文字を検出します。

変更前のコード:

if 0x20 <= b && b != '\\' && b != '"' && b != '<' && b != '>' {
    i++
    continue
}

このif文は、現在のバイトbがエスケープ不要な文字であるかどうかをチェックしています。

  • 0x20 <= b: ASCIIのスペース文字(U+0020)以上の文字であること。これより小さい文字は制御文字であり、常にエスケープが必要です。
  • b != '\\': バックスラッシュではないこと。バックスラッシュはJSONのエスケープ文字なので、それ自体をエスケープする必要があります。
  • b != '"': ダブルクォーテーションではないこと。JSON文字列の区切り文字なので、エスケープが必要です。
  • b != '<': 小なり記号ではないこと。HTMLコンテキストでのXSS対策としてエスケープが必要です。
  • b != '>': 大なり記号ではないこと。HTMLコンテキストでのXSS対策としてエスケープが必要です。

この条件を満たす文字は、そのまま出力され、i++で次の文字に進みます。しかし、この条件には&が含まれていなかったため、&はエスケープされずにそのまま出力されていました。

変更後のコード:

if 0x20 <= b && b != '\\' && b != '"' && b != '<' && b != '>' && b != '&' {
    i++
    continue
}

追加された&& b != '&'により、&もエスケープが必要な文字として認識されるようになりました。これにより、&が検出された場合、上記のif条件はfalseとなり、後続のエスケープ処理ロジック(switch文など)によって\u0026として適切にエスケープされるようになります。

src/pkg/html/template/content_test.go および src/pkg/html/template/escape_test.go の変更

これらのファイルは、html/templateパッケージのテストケースを含んでいます。html/templateは、HTMLテンプレートにデータを安全に挿入するために、内部でjson.Marshalを利用することがあります。

変更されたテストケースでは、&を含む文字列がJSONエンコードされた際に、期待される出力が\u0026を含むように修正されています。

例えば、content_test.goの以下の行が変更されています。

  • " "foo%" O'Reilly &bar;""<b> \"foo%\" O'Reilly \u0026bar;"
  • "Hello, <b>World</b> &amp;tc!""Hello, <b>World</b> \u0026amp;tc!"
  • "greeting=H%69&addressee=(World)""greeting=H%69\u0026addressee=(World)"

これらの変更は、encoding/jsonの変更がhtml/templateの出力にどのように影響するか、そしてその影響が期待通りにセキュリティを向上させるものであることを検証しています。特に、&amp;のように既にHTMLエンティティとしてエスケープされている文字列に対しても、JSONエンコード時にはさらに&\u0026としてエスケープされることで、二重のエスケープが行われ、より堅牢なセキュリティが確保されることを示しています。

このコミットは、Goの標準ライブラリにおけるセキュリティと一貫性を向上させるための重要な修正であり、特にWebアプリケーション開発においてXSS脆弱性を防ぐ上で役立ちます。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (GitHub): https://github.com/golang/go
  • Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されているhttps://golang.org/cl/12708044はGerritの変更リストへのリンクです。)
  • JSON RFC 7159: https://www.rfc-editor.org/rfc/rfc7159 (JSONの正式な仕様)
  • HTML Living Standard: https://html.spec.whatwg.org/multipage/ (HTMLの仕様、特にエスケープに関する部分)
  • JavaScript仕様 (ECMAScript): https://tc39.es/ecma262/ (JavaScriptの仕様、特に文字列リテラルのエスケープに関する部分)
  • Russ CoxのブログやGoに関する発表資料 (一般的なGoの設計思想や背景を理解するため)
  • Brad FitzpatrickのGoに関する活動 (レビュー担当者として記載されているため)