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

[インデックス 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形式である」と見なされるバイト列には適用されていなかったためです。

このコミットでは、以下の変更が導入されました。

  1. compact 関数の導入: 既存の Compact 関数をラップする形で、内部的に compact という新しい関数が導入されました。この compact 関数は、escape というブール型の引数を追加で受け取ります。
    • escapetrue の場合、JSONバイト列内の <>& 文字をそれぞれ \u003c\u003e\u0026 にエスケープします。
    • escapefalse の場合、従来通り空白文字の除去のみを行います。
  2. Marshaler 出力へのエスケープ適用: encode.go 内の reflectValueQuoted メソッドにおいて、MarshalerMarshalJSON メソッドから返したバイト列を処理する際に、新しく導入された compact 関数を escape 引数を true にして呼び出すように変更されました。これにより、Marshaler の出力に対してもHTML特殊文字のエスケープが強制されるようになりました。
  3. テストケースの追加: encode_test.goTestMarshalerEscaping という新しいテストケースが追加されました。このテストは、Marshaler を実装した型が <&> という文字列を含むJSONを返した場合に、それが正しく \u003c\u0026\u003e とエスケープされることを検証します。

この修正により、encoding/json パッケージは、Goの値を直接マーシャリングする場合でも、json.Marshaler を介してカスタムマーシャリングを行う場合でも、一貫してHTML特殊文字のエスケープ処理を行うようになり、WebアプリケーションにおけるXSS脆弱性のリスクを低減しました。

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

このコミットでは、主に以下の3つのファイルが変更されています。

  1. src/pkg/encoding/json/encode.go
  2. src/pkg/encoding/json/encode_test.go
  3. 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 インターフェースが返すバイト列 be.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 関数内のループで、escapetrue であり、かつ現在の文字 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>>43c&0xFC となり、\u003c が生成されます。
    • start = i + 1: 次の書き込み開始位置を、エスケープされた文字の直後に設定します。

この変更により、json.Marshaler が返すJSONデータが compact 関数を escape=true で通過するようになり、HTML特殊文字が自動的にエスケープされるようになりました。これにより、開発者が Marshaler を実装する際に、手動でこれらのエスケープを考慮する必要がなくなり、セキュリティが向上しました。

関連リンク

参考にした情報源リンク