[インデックス 12297] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/gob
パッケージにおける、入力文字列の長さに関する堅牢性(hardening)を強化するものです。具体的には、デコード時に不正に大きな長さが指定された場合に発生しうるサービス拒否(DoS)攻撃やメモリ枯渇を防ぐための対策が施されています。
コミット
commit 1f0f459a163eb3a1f15b2ad50a6a80c49e8f87e0
Author: David Symonds <dsymonds@golang.org>
Date: Thu Mar 1 15:57:54 2012 +1100
encoding/gob: more hardening for lengths of input strings.
Fixes #3160.
R=r
CC=golang-dev
https://golang.org/cl/5716046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1f0f459a163eb3a1f15b2ad50a6a80c49e8f87e0
元コミット内容
encoding/gob: more hardening for lengths of input strings.
このコミットは、encoding/gob
パッケージにおいて、入力される文字列やバイトスライス、スライスの長さに対する堅牢性をさらに高めることを目的としています。これは、特に不正な入力データが与えられた際に、過剰なメモリ割り当てや処理時間の増加を防ぐためのセキュリティ強化策です。
変更の背景
この変更は、GoのIssue #3160「fuzz testing failure」を修正するために行われました。ファズテスト(fuzz testing)は、プログラムにランダムな、あるいは予期せぬ入力を与えることで、潜在的なバグや脆弱性(特にクラッシュやメモリリークなど)を発見するテスト手法です。
encoding/gob
はGo言語のデータ構造をシリアライズ・デシリアライズするためのパッケージであり、ネットワーク経由でのデータ交換や永続化によく利用されます。このようなシリアライズ・デシリアライズのメカニズムは、外部からの入力に依存するため、悪意のあるデータや破損したデータが与えられた場合に脆弱性を持つ可能性があります。
Issue #3160で報告されたファズテストの失敗は、gob
デコーダが非常に大きな、あるいは負の長さを指定された場合に、過剰なメモリを割り当てようとしたり、非効率な処理を行ったりする問題を示唆していました。これにより、攻撃者が意図的に巨大な長さを指定したデータを送信することで、サーバーのメモリを枯渇させたり、処理を停止させたりするサービス拒否(DoS)攻撃が可能になるリスクがありました。
このコミットは、このような潜在的な脆弱性に対処し、gob
デコーダが入力データの長さをより厳密に検証することで、堅牢性とセキュリティを向上させることを目的としています。
前提知識の解説
Go言語の encoding/gob
パッケージ
encoding/gob
は、Go言語のデータ構造をバイナリ形式でエンコード(シリアライズ)およびデコード(デシリアライズ)するためのパッケージです。Goの構造体、スライス、マップ、プリミティブ型などを効率的にバイトストリームに変換し、またその逆を行うことができます。
- 自己記述型 (Self-describing):
gob
は、データだけでなく、そのデータの型情報もストリームに含めることができます。これにより、受信側は事前に型を知らなくてもデータをデコードできます。 - 効率性:
gob
は、Goの型システムに最適化されており、リフレクションを効果的に利用して効率的なエンコード/デコードを実現します。 - 用途: RPC(Remote Procedure Call)におけるデータ転送、設定ファイルの保存、キャッシュなど、Goプログラム間でデータをやり取りする様々な場面で利用されます。
シリアライズとデシリアライズにおけるセキュリティリスク
データ構造をバイナリ形式に変換するシリアライズと、その逆のデシリアライズは、多くのプログラミング言語やシステムで利用されます。しかし、このプロセスには固有のセキュリティリスクが伴います。
- 不正な入力: 攻撃者が意図的に不正な形式のデータや、異常に大きな値を埋め込んだデータを送信する可能性があります。
- メモリ枯渇 (Memory Exhaustion): デシリアライザが、入力データに含まれる「長さ」の情報を無制限に信頼し、その長さに応じてメモリを割り当てようとすると、攻撃者は非常に大きな長さを指定することで、ターゲットシステムのメモリを使い果たさせ、サービスを停止させることができます。これはサービス拒否(DoS)攻撃の一種です。
- 無限ループ/処理時間の増加: 不正なデータ構造や循環参照などが含まれている場合、デシリアライザが無限ループに陥ったり、処理に異常に長い時間がかかったりする可能性があります。これもDoS攻撃につながります。
- コード実行 (Code Execution): より高度な攻撃では、デシリアライズの過程で、攻撃者が制御するコードが実行される脆弱性(例: JavaのRCE脆弱性)が存在することもありますが、
gob
はGoの型システムに厳密に従うため、このリスクは比較的低いとされています。
ファズテスト (Fuzz Testing)
ファズテストは、ソフトウェアの入力に対して、ランダムなデータや半構造化されたデータを大量に与え、その応答(クラッシュ、アサーション失敗、メモリリークなど)を監視することで、バグや脆弱性を発見する自動テスト手法です。
- 目的: 予期せぬ入力に対するソフトウェアの堅牢性を評価し、開発者が想定していなかったエッジケースや脆弱性を発見すること。
- 方法:
- テスト対象のプログラムに、無効な、予期せぬ、またはランダムなデータを入力として与える。
- プログラムの動作を監視し、クラッシュ、ハング、メモリリーク、不正な出力などの異常を検出する。
- 異常が検出された場合、その異常を引き起こした入力データを記録し、開発者が問題をデバッグできるようにする。
- 重要性: 特にパーサー、プロトコル実装、ファイルフォーマット処理など、外部からの入力に依存するコンポーネントのテストにおいて非常に有効です。
このコミットは、ファズテストによって発見された gob
デコーダの脆弱性に対処するものです。
技術的詳細
このコミットの主要な目的は、encoding/gob
デコーダが、入力ストリームから読み取るバイトスライス、文字列、およびスライスの「長さ」を処理する際の堅牢性を向上させることです。以前の実装では、デコードされる要素の長さが入力バッファの残りのサイズを超えているかどうかのチェックが不十分でした。これにより、デコーダが非常に大きな長さを読み取った場合、存在しないメモリ領域を読み取ろうとしたり、過剰なメモリを割り当てようとしたりする可能性がありました。
具体的な脆弱性は以下のシナリオで発生しえます。
- 巨大な長さの指定: 攻撃者が
gob
ストリーム内に、例えばuint64
の最大値に近いような非常に大きな長さをエンコードして送信します。 - メモリ割り当ての試行: デコーダがこの巨大な長さを読み取り、その長さのバイトスライスや文字列、スライスを
make
関数などで割り当てようとします。 - メモリ枯渇/クラッシュ: システムに利用可能なメモリよりもはるかに大きなメモリ割り当てが要求されるため、システムはメモリ不足に陥り、プロセスがクラッシュするか、システム全体のパフォーマンスが著しく低下し、サービス拒否状態になります。
このコミットでは、この問題を解決するために、デコードされた長さが現在の入力バッファの残りのサイズを超えていないかを厳密にチェックするガードを追加しています。
state.decodeUint()
は、gob
ストリームから符号なし整数(長さ情報)を読み取る関数です。state.b.Len()
は、現在のデコード状態における入力バッファに残っているバイト数を示します。
変更の核心は、n > uint64(state.b.Len())
というチェックです。これは、「デコードされた長さ n
が、まだ読み取られていない入力データの総量 state.b.Len()
を超えている場合、それは不正なデータである」というロジックに基づいています。もしデコードされた長さが残りの入力データよりも大きい場合、その長さのデータを読み取ることは不可能であり、不正な入力と判断してエラーを発生させるべきです。
また、型名の長さについても同様のチェックが追加されています。型名が非常に長い場合も、メモリ枯渇や処理時間の増加につながる可能性があるため、上限が設けられています。
コアとなるコードの変更箇所
src/pkg/encoding/gob/codec_test.go
--- a/src/pkg/encoding/gob/codec_test.go
+++ b/src/pkg/encoding/gob/codec_test.go
@@ -1455,11 +1455,14 @@ func TestFuzz(t *testing.T) {
func TestFuzzRegressions(t *testing.T) {
// An instance triggering a type name of length ~102 GB.
testFuzz(t, 1328492090837718000, 100, new(float32))
+ // An instance triggering a type name of 1.6 GB.
+ // Commented out because it takes 5m to run.
+ //testFuzz(t, 1330522872628565000, 100, new(int))
}
func testFuzz(t *testing.T, seed int64, n int, input ...interface{}) {
-\tt.Logf("seed=%d n=%d\n", seed, n)\n \tfor _, e := range input {\n+\t\tt.Logf("seed=%d n=%d e=%T", seed, n, e)\n \t\trng := rand.New(rand.NewSource(seed))\n \t\tfor i := 0; i < n; i++ {\n \t\t\tencFuzzDec(rng, e)\n```
### `src/pkg/encoding/gob/decode.go`
```diff
--- a/src/pkg/encoding/gob/decode.go
+++ b/src/pkg/encoding/gob/decode.go
@@ -392,12 +392,12 @@ func decUint8Slice(i *decInstr, state *decoderState, p unsafe.Pointer) {
}
p = *(*unsafe.Pointer)(p)
}
-\tn := int(state.decodeUint())\n-\tif n < 0 {\n-\t\terrorf("negative length decoding []byte")\n+\tn := state.decodeUint()\n+\tif n > uint64(state.b.Len()) {\n+\t\terrorf("length of []byte exceeds input size (%d bytes)", n)\n \t}\n \tslice := (*[]uint8)(p)\n-\tif cap(*slice) < n {\n+\tif uint64(cap(*slice)) < n {\n \t\t*slice = make([]uint8, n)\n \t} else {\n \t\t*slice = (*slice)[0:n]\n@@ -417,7 +417,11 @@ func decString(i *decInstr, state *decoderState, p unsafe.Pointer) {\n }
p = *(*unsafe.Pointer)(p)
}
-\tb := make([]byte, state.decodeUint())\n+\tn := state.decodeUint()\n+\tif n > uint64(state.b.Len()) {\n+\t\terrorf("string length exceeds input size (%d bytes)", n)\n+\t}\n+\tb := make([]byte, n)\n \tstate.b.Read(b)\n \t// It would be a shame to do the obvious thing here,\n \t//\t*(*string)(p) = string(b)\n@@ -647,7 +651,11 @@ func (dec *Decoder) ignoreMap(state *decoderState, keyOp, elemOp decOp) {\n // decodeSlice decodes a slice and stores the slice header through p.\n // Slices are encoded as an unsigned length followed by the elements.\n func (dec *Decoder) decodeSlice(atyp reflect.Type, state *decoderState, p uintptr, elemOp decOp, elemWid uintptr, indir, elemIndir int, ovfl error) {\n-\tn := int(uintptr(state.decodeUint()))\n+\tnr := state.decodeUint()\n+\tif nr > uint64(state.b.Len()) {\n+\t\terrorf("length of slice exceeds input size (%d elements)", nr)\n+\t}\n+\tn := int(nr)\n \tif indir > 0 {\n \t\tup := unsafe.Pointer(p)\n \t\tif *(*unsafe.Pointer)(up) == nil {\n@@ -702,6 +710,9 @@ func (dec *Decoder) decodeInterface(ityp reflect.Type, state *decoderState, p ui\n \t\t*(*[2]uintptr)(unsafe.Pointer(p)) = ivalue.InterfaceData()\n \t\treturn\n \t}\n+\tif len(name) > 1024 {\n+\t\terrorf("name too long (%d bytes): %.20q...", len(name), name)\n+\t}\n \t// The concrete type must be registered.\n \ttyp, ok := nameToConcreteType[name]\n \tif !ok {\n```
## コアとなるコードの解説
### `src/pkg/encoding/gob/decode.go` の変更点
このファイルは `gob` デコードの主要なロジックを含んでいます。変更は主に、バイトスライス、文字列、およびスライスの長さをデコードする関数に集中しています。
1. **`decUint8Slice` 関数(`[]byte` のデコード)**
- 変更前: `n := int(state.decodeUint())` で長さを読み取り、`if n < 0` で負の長さをチェックしていました。
- 変更後:
- `n := state.decodeUint()`: 長さを `uint64` として読み取ります。これにより、非常に大きな正の長さも適切に扱えます。
- `if n > uint64(state.b.Len())`: **この行が最も重要です。** デコードされた長さ `n` が、現在の入力バッファに残っているバイト数 `state.b.Len()` を超えている場合、エラー `length of []byte exceeds input size` を発生させます。これにより、存在しないデータを読み取ろうとする試みや、過剰なメモリ割り当ての要求を防ぎます。
- `if uint64(cap(*slice)) < n`: スライスの容量チェックも `uint64` にキャストして比較することで、大きな長さに対する正確な比較を保証しています。
2. **`decString` 関数(`string` のデコード)**
- 変更前: `b := make([]byte, state.decodeUint())` で長さを読み取り、直接バイトスライスを作成していました。ここには長さの妥当性チェックがありませんでした。
- 変更後:
- `n := state.decodeUint()`: 長さを `uint64` として読み取ります。
- `if n > uint64(state.b.Len())`: **ここでも同様に、** デコードされた文字列の長さ `n` が、入力バッファの残りのサイズを超えている場合にエラー `string length exceeds input size` を発生させます。これにより、不正な長さの文字列デコードによるメモリ枯渇を防ぎます。
- `b := make([]byte, n)`: 妥当性が確認された長さ `n` でバイトスライスを作成します。
3. **`decodeSlice` 関数(任意のスライスのデコード)**
- 変更前: `n := int(uintptr(state.decodeUint()))` で長さを読み取り、`int` にキャストしていました。
- 変更後:
- `nr := state.decodeUint()`: 長さを `uint64` として読み取ります。
- `if nr > uint64(state.b.Len())`: **ここでも同様に、** デコードされたスライスの要素数 `nr` が、入力バッファの残りのサイズを超えている場合にエラー `length of slice exceeds input size` を発生させます。これにより、不正な長さのスライスデコードによるメモリ枯渇を防ぎます。
- `n := int(nr)`: 妥当性が確認された `nr` を `int` にキャストして使用します。
4. **`decodeInterface` 関数(インターフェースのデコードにおける型名チェック)**
- 追加: `if len(name) > 1024`
- インターフェースのデコード時には、具体的な型名が `gob` ストリームから読み取られます。この型名が非常に長い場合も、メモリ使用量や処理時間に影響を与える可能性があります。
- この変更では、読み取られた型名 `name` の長さが1024バイトを超えた場合にエラー `name too long` を発生させます。これにより、異常に長い型名による潜在的なDoS攻撃を防ぎます。1024バイトという具体的な値は、一般的な型名としては十分な長さであり、かつ異常な長さを検出するための閾値として設定されています。
これらの変更により、`gob` デコーダは、入力ストリームから読み取る長さ情報が、実際に利用可能な入力データの範囲内にあることを厳密に検証するようになります。これにより、不正な入力データによるメモリ枯渇やサービス拒否攻撃のリスクが大幅に軽減されます。
### `src/pkg/encoding/gob/codec_test.go` の変更点
このファイルは `gob` パッケージのテストコードを含んでいます。
1. **`TestFuzzRegressions` 関数**
- 以前はコメントアウトされていた `testFuzz(t, 1330522872628565000, 100, new(int))` の行が、コメントアウトされたままですが、その理由が追記されています。
- `// An instance triggering a type name of 1.6 GB.`
- `// Commented out because it takes 5m to run.`
- これは、この特定のファズテストのシードが、以前は1.6GBもの型名を生成しようとし、実行に5分もかかるような非常に重いケースであったことを示しています。今回の変更によって、このような巨大な型名がデコードされる前に `decodeInterface` 関数でエラーが検出されるようになるため、このテストケースはもはやクラッシュを引き起こすことはありませんが、実行時間の問題からコメントアウトされたままになっています。これは、今回の修正が実際に効果を発揮していることを間接的に示しています。
2. **`testFuzz` 関数**
- `t.Logf` の出力フォーマットが変更されました。
- 変更前: `t.Logf("seed=%d n=%d\n", seed, n)`
- 変更後: `t.Logf("seed=%d n=%d e=%T", seed, n, e)`
- これにより、ファズテストのログに、現在テストしている入力データの型 (`e=%T`) が追加されるようになり、デバッグ情報がより豊富になりました。これは直接的なセキュリティ修正とは関係ありませんが、テストの可視性とデバッグのしやすさを向上させるための改善です。
これらのテストの変更は、今回の堅牢性強化がファズテストによって発見された問題に対処していること、およびその修正が期待通りに機能していることを確認するためのものです。
## 関連リンク
- Go Issue #3160: [https://go.dev/issue/3160](https://go.dev/issue/3160) (Goの公式Issueトラッカー)
- Go CL 5716046: [https://go.googlesource.com/go/+/5716046](https://go.googlesource.com/go/+/5716046) (Gerrit Code Reviewの変更リスト)
## 参考にした情報源リンク
- Go Issue #3160 (goissues.org): [https://goissues.org/issue/3160](https://goissues.org/issue/3160)
- Go言語の `encoding/gob` パッケージのドキュメント: [https://pkg.go.dev/encoding/gob](https://pkg.go.dev/encoding/gob)
- ファズテストに関する一般的な情報 (例: OWASP Fuzzing): [https://owasp.org/www-community/Fuzzing](https://owasp.org/www-community/Fuzzing) (一般的な情報源であり、このコミットに直接関連するものではありませんが、前提知識として参照しました)
- Go言語の `unsafe` パッケージのドキュメント: [https://pkg.go.dev/unsafe](https://pkg.go.dev/unsafe) (コード内で `unsafe.Pointer` が使用されているため、関連知識として参照)
- Go言語の `reflect` パッケージのドキュメント: [https://pkg.go.dev/reflect](https://pkg.go.dev/reflect) (コード内で `reflect.Type` が使用されているため、関連知識として参照)