[インデックス 12300] ファイルの概要
このコミットは、Go言語の encoding/json パッケージにおけるJSONマーシャリング時のエスケープ処理に関する修正です。具体的には、Marshaler インターフェースを実装する型が返すJSON出力において、HTMLに埋め込む際に問題となる可能性のある特殊文字(<, >, &)が適切にエスケープされるように変更されました。これにより、JSONデータがHTMLコンテキストで安全に利用できるようになります。
コミット
commit 99e45e49b7438bc45a6dd09fb2636dde74ef5d33
Author: David Symonds <dsymonds@golang.org>
Date: Thu Mar 1 17:41:59 2012 +1100
encoding/json: escape output from Marshalers.
Fixes #3127.
R=rsc, r
CC=golang-dev
https://golang.org/cl/5707054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/99e45e49b7438bc45a6dd09fb2636dde74ef5d33
元コミット内容
encoding/json: escape output from Marshalers.
Fixes #3127.
R=rsc, r
CC=golang-dev
https://golang.org/cl/5707054
変更の背景
この変更は、Go言語の encoding/json パッケージが抱えていた、json.Marshaler インターフェースを実装するカスタム型が生成するJSON文字列が、HTMLコンテキストで安全でない可能性があった問題(Issue #3127)を解決するために行われました。
従来の encoding/json パッケージでは、Goの構造体などをJSONにマーシャリングする際、デフォルトではHTML特殊文字(<, >, &)を \uXXXX 形式でエスケープしていました。これは、JSON文字列を直接HTMLの <script> タグ内などに埋め込む場合に、スクリプトインジェクションなどのクロスサイトスクリプティング(XSS)攻撃を防ぐための重要なセキュリティ対策です。
しかし、json.Marshaler インターフェースを独自に実装した場合、その実装が返すJSONバイト列は、encoding/json パッケージのデフォルトのエスケープ処理をスキップしていました。つまり、Marshaler が < や >、& といった文字を含むJSONを返した場合、それらの文字はエスケープされずにそのまま出力されていました。
この挙動は、特にWebアプリケーションにおいて、ユーザーが入力したデータが json.Marshaler を通じてJSONとして出力され、それがHTMLページに埋め込まれるようなシナリオで深刻な脆弱性につながる可能性がありました。例えば、ユーザーが <script>alert('XSS')</script> のような文字列を入力し、それがエスケープされずにJSONとして出力されると、ブラウザがその文字列をスクリプトとして解釈し、悪意のあるコードが実行されてしまう恐れがありました。
このコミットは、このセキュリティ上のギャップを埋め、Marshaler が返すJSONデータに対しても、デフォルトのマーシャリングと同様にHTML特殊文字のエスケープ処理を適用することで、より堅牢なセキュリティを確保することを目的としています。
前提知識の解説
JSON (JavaScript Object Notation)
JSONは、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。JavaScriptのオブジェクトリテラルをベースにしていますが、言語に依存しないデータ形式として広く利用されています。Web APIのデータ送受信、設定ファイルの記述など、様々な用途で使われています。
JSONのエスケープ処理
JSON文字列内では、特定の文字(例: ダブルクォーテーション "、バックスラッシュ \、制御文字など)は、JSONの構文を壊さないように、または特殊な意味を持つ文字として解釈されないように、エスケープシーケンス(例: \", \\, \n)を用いて表現されます。
Webアプリケーションの文脈では、JSONデータをHTMLドキュメントに埋め込む際に、さらに追加のエスケープが必要になる場合があります。特に、HTMLの特殊文字である < (小なり記号), > (大なり記号), & (アンパサンド) は、HTMLパーサーによって特殊な意味を持つ文字として解釈されるため、これらがJSON文字列内にそのまま含まれていると、HTMLの構造を破壊したり、XSS脆弱性を引き起こしたりする可能性があります。
例えば、<script> タグ内にJSONデータが埋め込まれる場合、JSON文字列内の </script> という部分がHTMLの終了タグとして解釈されてしまい、その後の文字列がスクリプトとして実行される可能性があります。これを防ぐために、これらの文字は \u003c (<), \u003e (>), \u0026 (&) のようにUnicodeエスケープシーケンスで表現されることが推奨されます。
encoding/json パッケージ (Go言語)
Go言語の標準ライブラリに含まれる encoding/json パッケージは、Goのデータ構造とJSONデータの間で変換(マーシャリングとアンマーシャリング)を行う機能を提供します。
json.Marshal: Goの値をJSONバイト列に変換します。json.Unmarshal: JSONバイト列をGoの値に変換します。json.Marshalerインターフェース:
このインターフェースを実装する型は、type Marshaler interface { MarshalJSON() ([]byte, error) }MarshalJSONメソッドを独自に定義することで、その型がJSONにマーシャリングされる際の挙動をカスタマイズできます。MarshalJSONメソッドは、その型のJSON表現となるバイト列を返します。
Issue #3127
Go言語のIssueトラッカーで報告された問題で、json.Marshaler インターフェースを実装した型が返すJSON文字列が、HTML特殊文字のエスケープ処理をスキップしてしまうというバグです。このコミットはこのIssueを解決するために作成されました。
技術的詳細
このコミットの技術的な核心は、encoding/json パッケージ内でJSONバイト列をコンパクト化(不要な空白文字を除去)する際に、同時にHTML特殊文字のエスケープ処理を行うように変更した点です。
変更前は、json.Marshaler が返すバイト列は Compact 関数によって単に空白が除去されるだけで、HTML特殊文字のエスケープは行われませんでした。これは、json.Marshal が内部的にGoの値をJSONに変換する際に適用されるエスケープルールが、Marshaler インターフェースを介して提供される「既にJSON形式である」と見なされるバイト列には適用されていなかったためです。
このコミットでは、以下の変更が導入されました。
compact関数の導入: 既存のCompact関数をラップする形で、内部的にcompactという新しい関数が導入されました。このcompact関数は、escapeというブール型の引数を追加で受け取ります。escapeがtrueの場合、JSONバイト列内の<、>、&文字をそれぞれ\u003c、\u003e、\u0026にエスケープします。escapeがfalseの場合、従来通り空白文字の除去のみを行います。
Marshaler出力へのエスケープ適用:encode.go内のreflectValueQuotedメソッドにおいて、MarshalerがMarshalJSONメソッドから返したバイト列を処理する際に、新しく導入されたcompact関数をescape引数をtrueにして呼び出すように変更されました。これにより、Marshalerの出力に対してもHTML特殊文字のエスケープが強制されるようになりました。- テストケースの追加:
encode_test.goにTestMarshalerEscapingという新しいテストケースが追加されました。このテストは、Marshalerを実装した型が<&>という文字列を含むJSONを返した場合に、それが正しく\u003c\u0026\u003eとエスケープされることを検証します。
この修正により、encoding/json パッケージは、Goの値を直接マーシャリングする場合でも、json.Marshaler を介してカスタムマーシャリングを行う場合でも、一貫してHTML特殊文字のエスケープ処理を行うようになり、WebアプリケーションにおけるXSS脆弱性のリスクを低減しました。
コアとなるコードの変更箇所
このコミットでは、主に以下の3つのファイルが変更されています。
src/pkg/encoding/json/encode.gosrc/pkg/encoding/json/encode_test.gosrc/pkg/encoding/json/indent.go
src/pkg/encoding/json/encode.go の変更
--- a/src/pkg/encoding/json/encode.go
+++ b/src/pkg/encoding/json/encode.go
@@ -260,7 +260,7 @@ func (e *encodeState) reflectValueQuoted(v reflect.Value, quoted bool) {
tb, err := m.MarshalJSON()
if err == nil {
// copy JSON into buffer, checking validity.
- err = Compact(&e.Buffer, b)
+ err = compact(&e.Buffer, b, true)
}
if err != nil {
e.error(&MarshalerError{v.Type(), err})
Compact(&e.Buffer, b)の呼び出しがcompact(&e.Buffer, b, true)に変更されました。- これは、
Marshalerインターフェースが返すバイト列bをe.Bufferにコピーする際に、新しく導入されたcompact関数を使用し、trueを渡すことでHTML特殊文字のエスケープを有効にしていることを示します。
src/pkg/encoding/json/encode_test.go の変更
--- a/src/pkg/encoding/json/encode_test.go
+++ b/src/pkg/encoding/json/encode_test.go
@@ -167,3 +167,22 @@ func TestRefValMarshal(t *testing.T) {
tt.Errorf("got %q, want %q", got, want)
}
}
+
+// C implements Marshaler and returns unescaped JSON.
+type C int
+
+func (C) MarshalJSON() ([]byte, error) {
+ return []byte(`"<&>"`), nil
+}
+
+func TestMarshalerEscaping(t *testing.T) {
+ var c C
+ const want = `"\u003c\u0026\u003e"`
+ b, err := Marshal(c)
+ if err != nil {
+ t.Fatalf("Marshal: %v", err)
+ }
+ if got := string(b); got != want {
+ t.Errorf("got %q, want %q", got, want)
+ }
+}
TestMarshalerEscapingという新しいテスト関数が追加されました。Cというカスタム型が定義され、そのMarshalJSONメソッドは"<&>"という文字列を返します。- このテストは、
Marshal(c)の結果が"\u003c\u0026\u003e"となることを期待しており、<,&,>がそれぞれUnicodeエスケープシーケンスに変換されていることを検証しています。
src/pkg/encoding/json/indent.go の変更
--- a/src/pkg/encoding/json/indent.go
+++ b/src/pkg/encoding/json/indent.go
@@ -9,11 +9,24 @@ import "bytes"
// Compact appends to dst the JSON-encoded src with
// insignificant space characters elided.
func Compact(dst *bytes.Buffer, src []byte) error {
- return compact(dst, src, false)
+ return compact(dst, src, false)
+}
+
+func compact(dst *bytes.Buffer, src []byte, escape bool) error {
origLen := dst.Len()
var scan scanner
scan.reset()
start := 0
for i, c := range src {
+ if escape && (c == '<' || c == '>' || c == '&') {
+ if start < i {
+ dst.Write(src[start:i])
+ }
+ dst.WriteString(`\u00`)
+ dst.WriteByte(hex[c>>4])
+ dst.WriteByte(hex[c&0xF])
+ start = i + 1
+ }
v := scan.step(&scan, int(c))
if v >= scanSkipSpace {
if v == scanError {
- 既存の
Compact関数が、新しく定義されたcompact関数をescape引数をfalseにして呼び出すように変更されました。これにより、Compact関数自体の挙動は変更されず、従来通り空白文字の除去のみを行います。 compactという新しい関数が追加されました。この関数はescapeというブール型の引数を持ちます。compact関数内のループで、escapeがtrueであり、かつ現在の文字cが<、>、または&のいずれかである場合に、その文字を\u00XX形式のUnicodeエスケープシーケンスに変換してdstバッファに書き込むロジックが追加されました。hex配列は、バイト値を16進数文字に変換するために使用されます。
コアとなるコードの解説
このコミットの最も重要な変更は、src/pkg/encoding/json/indent.go に追加された compact 関数とそのエスケープロジックです。
func compact(dst *bytes.Buffer, src []byte, escape bool) error {
origLen := dst.Len()
var scan scanner
scan.reset()
start := 0
for i, c := range src {
if escape && (c == '<' || c == '>' || c == '&') {
if start < i {
dst.Write(src[start:i])
}
dst.WriteString(`\u00`)
dst.WriteByte(hex[c>>4])
dst.WriteByte(hex[c&0xF])
start = i + 1
}
v := scan.step(&scan, int(c))
if v >= scanSkipSpace {
if v == scanError {
// ... (エラー処理)
}
// ... (空白文字の処理)
}
}
// ... (残りの処理)
return nil
}
compact関数の役割: この関数は、JSONバイト列srcを受け取り、不要な空白文字を削除してdstバッファに書き込みます。追加されたescape引数がtrueの場合、HTML特殊文字のエスケープも同時に行います。- エスケープロジック:
if escape && (c == '<' || c == '>' || c == '&'): この条件は、escapeフラグがtrueであり、かつ現在の文字cがエスケープ対象のHTML特殊文字(<,>,&)のいずれかである場合に真となります。if start < i { dst.Write(src[start:i]) }: エスケープ対象の文字が見つかるまでの部分文字列をdstバッファに書き込みます。dst.WriteString(\u00): Unicodeエスケープシーケンスのプレフィックス\u00を書き込みます。dst.WriteByte(hex[c>>4]): 文字cの上位4ビットを16進数に変換し、対応する文字(例:3for<)を書き込みます。dst.WriteByte(hex[c&0xF]): 文字cの下位4ビットを16進数に変換し、対応する文字(例:cfor<)を書き込みます。- 例:
<(ASCII 60, 16進数 0x3C) の場合、c>>4は3、c&0xFはCとなり、\u003cが生成されます。
- 例:
start = i + 1: 次の書き込み開始位置を、エスケープされた文字の直後に設定します。
この変更により、json.Marshaler が返すJSONデータが compact 関数を escape=true で通過するようになり、HTML特殊文字が自動的にエスケープされるようになりました。これにより、開発者が Marshaler を実装する際に、手動でこれらのエスケープを考慮する必要がなくなり、セキュリティが向上しました。
関連リンク
- Go Issue #3127: https://github.com/golang/go/issues/3127
- Go CL 5707054: https://golang.org/cl/5707054
参考にした情報源リンク
- Go言語
encoding/jsonパッケージ公式ドキュメント: https://pkg.go.dev/encoding/json - JSON (JavaScript Object Notation) 公式サイト: https://www.json.org/json-ja.html
- クロスサイトスクリプティング (XSS) - MDN Web Docs: https://developer.mozilla.org/ja/docs/Glossary/Cross-site_scripting
- Unicodeエスケープシーケンス - Wikipedia: https://ja.wikipedia.org/wiki/Unicode%E3%82%A8%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%97%E3%82%B7%E3%83%BC%E3%82%B1%E3%83%B3%E3%82%B9