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

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

このコミットは、Go言語のencoding/jsonパッケージにおいて、JSON文字列内のUnicode文字U+2028 (LINE SEPARATOR) とU+2029 (PARAGRAPH SEPARATOR) をエスケープするように変更を加えるものです。これにより、JSONP (JSON with Padding) として利用される際に発生しうるセキュリティ上の脆弱性に対処します。

コミット

commit d754647963d41bcd96ea4d12d824f01e8c50f076
Author: David Symonds <dsymonds@golang.org>
Date:   Fri Jul 12 14:35:55 2013 +1000

    encoding/json: escape U+2028 and U+2029.
    
    Fixes #5836.
    
    R=golang-dev, bradfitz, r, rsc
    CC=golang-dev
    https://golang.org/cl/10883045

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

https://github.com/golang/go/commit/d754647963d41bcd96ea4d12d824f01e8c50f076

元コミット内容

encoding/jsonパッケージにおいて、Unicode文字U+2028 (LINE SEPARATOR) およびU+2029 (PARAGRAPH SEPARATOR) をJSON出力時にエスケープする変更。これはIssue #5836を修正するものです。

変更の背景

この変更の背景には、JSONとJSONPのセキュリティ上の問題があります。

JSON (JavaScript Object Notation) は、データ交換のための軽量なフォーマットであり、JavaScriptのオブジェクトリテラル構文に基づいています。しかし、JSONはJavaScriptの厳密なサブセットではありません。特に、U+2028 (LINE SEPARATOR) とU+2029 (PARAGRAPH SEPARATOR) という2つのUnicode文字は、JSON文字列内では有効な文字として扱われますが、JavaScriptの文字列リテラル内では改行として解釈されます。

この違いが問題となるのは、JSONP (JSON with Padding) を使用する場合です。JSONPは、クロスドメイン通信を可能にするためのテクニックで、JSONデータをJavaScriptの関数呼び出しの引数としてラップして提供します。例えば、callback({"data": "value"})のような形式です。

もしJSONデータ内にエスケープされていないU+2028やU+2029が含まれていると、JSONPとしてブラウザで評価された際に、JavaScriptの構文エラーを引き起こす可能性があります。さらに深刻なのは、特定の状況下でこれらの文字が悪用され、スクリプトインジェクション攻撃(JSONPインジェクション)につながる可能性があることです。例えば、攻撃者が制御するデータがJSONPレスポンスに含まれ、その中にエスケープされていないU+2028やU+2029が含まれている場合、ブラウザがそのJSONPレスポンスをJavaScriptとして解釈する際に、意図しないコードが実行されてしまう可能性があります。

このコミットは、このような潜在的なセキュリティリスクを軽減するために、encoding/jsonパッケージがJSON文字列を生成する際に、U+2028とU+2029を常にUnicodeエスケープシーケンス(\u2028および\u2029)に変換するように修正しました。これにより、生成されるJSONがJavaScriptの文字列リテラルとして安全に評価されることが保証されます。

前提知識の解説

JSON (JavaScript Object Notation)

JSONは、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。JavaScriptのオブジェクトリテラル構文に由来しますが、言語に依存しないデータ形式として広く利用されています。キーと値のペアの集合(オブジェクト)や、順序付けられた値のリスト(配列)で構成されます。

JSONP (JSON with Padding)

JSONPは、Webブラウザの「同一生成元ポリシー(Same-Origin Policy)」を回避し、異なるドメイン間でデータをやり取りするための技術です。これは、<script>タグが異なるドメインのスクリプトを読み込むことができるという特性を利用します。JSONPでは、サーバーはJSONデータを直接返すのではなく、そのJSONデータをJavaScriptの関数呼び出しの引数としてラップして返します。クライアント側では、その関数が定義されており、サーバーからのレスポンスを受け取ると、その関数が実行され、引数として渡されたJSONデータが処理されます。

例: クライアント側で定義された関数: function myCallback(data) { console.log(data); } サーバーからのJSONPレスポンス: myCallback({"message": "Hello from another domain!"});

Unicodeエスケープシーケンス

Unicodeエスケープシーケンスは、Unicode文字をASCII文字の組み合わせで表現する方法です。JSONやJavaScriptでは、\uXXXXの形式で表現されます。ここでXXXXは4桁の16進数で、Unicodeコードポイントを表します。例えば、日本語の「あ」は\u3042とエスケープされます。

U+2028 (LINE SEPARATOR) と U+2029 (PARAGRAPH SEPARATOR)

これらはUnicodeの制御文字です。

  • U+2028 (LINE SEPARATOR): 行の区切りを示します。
  • U+2029 (PARAGRAPH SEPARATOR): 段落の区切りを示します。

これらの文字は、JSONの仕様上は文字列内で許可されています。しかし、JavaScriptのECMAScript 5.1以前の仕様では、文字列リテラル内にこれらの文字が直接含まれていると構文エラーとなります(ECMAScript 6以降では許可されていますが、古いブラウザとの互換性を考慮する必要があります)。このため、JSONPとしてJavaScriptエンジンで評価される際に問題を引き起こす可能性がありました。

HTMLEscape関数

Goのencoding/jsonパッケージには、HTMLEscapeという関数が存在します。この関数は、JSONエンコードされたバイト列をHTMLの<script>タグ内に安全に埋め込めるように、特定の文字(<>&)をUnicodeエスケープシーケンスに変換します。これは、ブラウザが<script>タグ内のHTMLエスケープを標準的に尊重しないという歴史的な理由によるものです。このコミットでは、このHTMLEscape関数もU+2028とU+2029をエスケープするように拡張されています。

技術的詳細

このコミットは、encoding/jsonパッケージのJSONエンコーディングロジックに、U+2028とU+2029のエスケープ処理を追加します。

主な変更点は以下の通りです。

  1. encode.goencodeState.stringメソッドの変更:

    • このメソッドは、Goの文字列をJSON文字列としてエンコードする主要なロジックを含んでいます。
    • 変更前は、通常の制御文字(タブ、改行など)やHTMLエスケープが必要な文字(<, >, &)のみをエスケープしていました。
    • 変更後、ループ内で各Unicodeルーン(文字)をチェックし、それがU+2028またはU+2029である場合に、\u2028または\u2029という形式のUnicodeエスケープシーケンスに変換して出力するように修正されました。
    • このエスケープは無条件に行われます。これは、JSONの仕様ではこれらの文字が有効であっても、JSONPとしてJavaScriptで評価される際の安全性を最優先するためです。
  2. encode.goHTMLEscape関数の変更:

    • HTMLEscape関数は、JSONエンコードされたバイト列をHTMLの<script>タグ内に安全に埋め込むために、特定の文字をエスケープします。
    • 変更前は、<>&のみをエスケープしていました。
    • 変更後、U+2028とU+2029もエスケープ対象に追加されました。これらの文字はUTF-8で3バイトシーケンス(E2 80 A8およびE2 80 A9)として表現されるため、バイト列を直接検査してこれらのシーケンスを検出・変換するロジックが追加されています。
  3. indent.gocompact関数の変更:

    • compact関数は、JSONバイト列をコンパクトな形式に整形する際に使用されます。
    • この関数もHTMLEscapeと同様に、バイト列を走査してU+2028とU+2029のUTF-8シーケンスを検出し、エスケープするロジックが追加されました。これは、HTMLEscapeが内部的にcompactを使用しているため、一貫したエスケープ処理を保証するためです。
  4. テストケースの追加と修正:

    • decode_test.goTestEscape関数が修正され、U+2028とU+2029を含む文字列が正しくエスケープされることを確認するテストが追加されました。
    • scanner_test.goTestCompactSeparatorsという新しいテスト関数が追加され、Compact関数がU+2028とU+2029を文字列内で正しくエスケープすることを確認しています。

これらの変更により、Goのencoding/jsonパッケージによって生成されるJSON文字列は、U+2028とU+2029が含まれる場合でも、JavaScriptの文字列リテラルとして安全に解釈されるようになり、JSONP利用時のセキュリティリスクが低減されます。

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

  • src/pkg/encoding/json/decode_test.go: テストケースの修正
  • src/pkg/encoding/json/encode.go: JSONエンコードの主要ロジックとHTMLEscape関数の変更
  • src/pkg/encoding/json/indent.go: JSON整形ロジックの変更
  • src/pkg/encoding/json/scanner_test.go: テストケースの追加

コアとなるコードの解説

src/pkg/encoding/json/encode.go

func (e *encodeState) string(s string) (int, error) メソッド内

		// U+2028 is LINE SEPARATOR.
		// U+2029 is PARAGRAPH SEPARATOR.
		// They are both technically valid characters in JSON strings,
		// but don't work in JSONP, which has to be evaluated as JavaScript,
		// and can lead to security holes there. It is valid JSON to
		// escape them, so we do so unconditionally.
		// See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
		if c == '\u2028' || c == '\u2029' {
			if start < i {
				e.WriteString(s[start:i])
			}
			e.WriteString(`\u202`)
			e.WriteByte(hex[c&0xF]) // c&0xF は下位4ビットを取り出す。U+2028とU+2029の最後の桁が8と9なので、これを利用して\u2028または\u2029を生成
			i += size
			start = i
			continue
		}

このコードブロックは、Goの文字列をJSON文字列に変換する際に、各Unicodeルーン(文字)を走査し、それがU+2028またはU+2029であるかをチェックします。もし該当する文字が見つかった場合、その文字を\u2028または\u2029というUnicodeエスケープシーケンスに変換して出力バッファに書き込みます。このエスケープは、JSONPとしてJavaScriptで評価される際のセキュリティ上の懸念から、無条件に行われます。

func HTMLEscape(dst *bytes.Buffer, src []byte) 関数内

		// Convert U+2028 and U+2029 (E2 80 A8 and E2 80 A9).
		if c == 0xE2 && i+2 < len(src) && src[i+1] == 0x80 && src[i+2]&^1 == 0xA8 {
			if start < i {
				dst.Write(src[start:i])
			}
			dst.WriteString(`\u202`)
			dst.WriteByte(hex[src[i+2]&0xF]) // src[i+2]はUTF-8シーケンスの3バイト目。その下位4ビットが8または9になる。
			start = i + 3
		}

このコードブロックは、HTMLEscape関数内で、入力バイト列srcを走査し、U+2028またはU+2029のUTF-8表現(E2 80 A8またはE2 80 A9)を検出します。c == 0xE2はUTF-8の最初のバイトがE2であること、src[i+1] == 0x80は2バイト目が80であること、src[i+2]&^1 == 0xA8は3バイト目がA8またはA9であることを効率的にチェックしています。検出された場合、これらの文字も\u2028または\u2029としてエスケープされます。

src/pkg/encoding/json/indent.go

func compact(dst *bytes.Buffer, src []byte, escape bool) error 関数内

HTMLEscape関数と同様のロジックがcompact関数にも追加されています。これは、JSONをコンパクトな形式に整形する際にも、U+2028とU+2029が適切にエスケープされることを保証するためです。

		// Convert U+2028 and U+2029 (E2 80 A8 and E2 80 A9).
		if c == 0xE2 && i+2 < len(src) && src[i+1] == 0x80 && src[i+2]&^1 == 0xA8 {
			if start < i {
				dst.Write(src[start:i])
			}
			dst.WriteString(`\u202`)
			dst.WriteByte(hex[src[i+2]&0xF])
			start = i + 3
		}

この変更により、encoding/jsonパッケージが生成するすべてのJSON出力において、U+2028とU+2029が安全にエスケープされるようになります。

関連リンク

参考にした情報源リンク