[インデックス 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のエスケープ処理を追加します。
主な変更点は以下の通りです。
-
encode.go
のencodeState.string
メソッドの変更:- このメソッドは、Goの文字列をJSON文字列としてエンコードする主要なロジックを含んでいます。
- 変更前は、通常の制御文字(タブ、改行など)やHTMLエスケープが必要な文字(
<
,>
,&
)のみをエスケープしていました。 - 変更後、ループ内で各Unicodeルーン(文字)をチェックし、それがU+2028またはU+2029である場合に、
\u2028
または\u2029
という形式のUnicodeエスケープシーケンスに変換して出力するように修正されました。 - このエスケープは無条件に行われます。これは、JSONの仕様ではこれらの文字が有効であっても、JSONPとしてJavaScriptで評価される際の安全性を最優先するためです。
-
encode.go
のHTMLEscape
関数の変更:HTMLEscape
関数は、JSONエンコードされたバイト列をHTMLの<script>
タグ内に安全に埋め込むために、特定の文字をエスケープします。- 変更前は、
<
、>
、&
のみをエスケープしていました。 - 変更後、U+2028とU+2029もエスケープ対象に追加されました。これらの文字はUTF-8で3バイトシーケンス(
E2 80 A8
およびE2 80 A9
)として表現されるため、バイト列を直接検査してこれらのシーケンスを検出・変換するロジックが追加されています。
-
indent.go
のcompact
関数の変更:compact
関数は、JSONバイト列をコンパクトな形式に整形する際に使用されます。- この関数も
HTMLEscape
と同様に、バイト列を走査してU+2028とU+2029のUTF-8シーケンスを検出し、エスケープするロジックが追加されました。これは、HTMLEscape
が内部的にcompact
を使用しているため、一貫したエスケープ処理を保証するためです。
-
テストケースの追加と修正:
decode_test.go
のTestEscape
関数が修正され、U+2028とU+2029を含む文字列が正しくエスケープされることを確認するテストが追加されました。scanner_test.go
にTestCompactSeparators
という新しいテスト関数が追加され、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が安全にエスケープされるようになります。
関連リンク
- Go Issue #5836: https://github.com/golang/go/issues/5836
- Go CL 10883045: https://golang.org/cl/10883045
参考にした情報源リンク
- JSON is not a JavaScript subset: http://timelessrepo.com/json-isnt-a-javascript-subset (コミットメッセージで参照されている記事)
- The JSON-P security vulnerability: https://www.scip.ch/en/?labs.20101021
- ECMAScript Language Specification - ECMA-262, 5.1 Edition: https://www.ecma-international.org/ecma-262/5.1/ (特に7.8.4 String Literalsのセクション)
- Unicode Character 'LINE SEPARATOR' (U+2028): https://www.compart.com/en/unicode/U+2028
- Unicode Character 'PARAGRAPH SEPARATOR' (U+2029): https://www.compart.com/en/unicode/U+2029
- UTF-8 - Wikipedia: https://ja.wikipedia.org/wiki/UTF-8
- JSONP - Wikipedia: https://ja.wikipedia.org/wiki/JSONP
- Same-origin policy - Wikipedia: https://ja.wikipedia.org/wiki/%E5%90%8C%E4%B8%80%E7%94%9F%E6%88%90%E5%85%83%E3%83%9D%E3%83%AA%E3%82%B7%E3%83%BC