[インデックス 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.go
src/pkg/encoding/json/encode_test.go
src/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進数に変換し、対応する文字(例:3
for<
)を書き込みます。dst.WriteByte(hex[c&0xF])
: 文字c
の下位4ビットを16進数に変換し、対応する文字(例:c
for<
)を書き込みます。- 例:
<
(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