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

[インデックス 18908] ファイルの概要

このコミットは、Go言語の標準ライブラリ encoding/base64 パッケージにおけるデコーダのバグ修正に関するものです。具体的には、Base64デコード時に末尾の不正なデータ(trailing garbage)を正しく検出できない問題を解決し、関連するテストケースを追加しています。

コミット

commit a2770af447a470cf34841f09d0a5bf23a9b5ea9e
Author: Rui Ueyama <ruiu@google.com>
Date:   Thu Mar 20 16:00:34 2014 +1100

    base64: fix bug that decoder fails to detect corruption

    Encoding.Decode() failed to detect trailing garbages if input contains "==" followed by garbage smaller than 3 bytes (for example, it failed to detect "x" in "AA==x"). This patch fixes the bug and adds a few tests.

    LGTM=nigeltao
    R=golang-codereviews, bradfitz, nigeltao
    CC=golang-codereviews
    https://golang.org/cl/75340044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a2770af447a470cf34841f09d0a5bf23a9b5ea9e

元コミット内容

Encoding.Decode() メソッドが、入力が "==" の後に3バイト未満の不正なデータ(例えば "AA==x" の "x")を含む場合に、その末尾の不正なデータを検出できないバグを修正します。このパッチはバグを修正し、いくつかのテストを追加します。

変更の背景

Base64エンコーディングは、バイナリデータをASCII文字列形式に変換するための一般的な方法です。この変換プロセスでは、元のバイナリデータのバイト数が3の倍数でない場合、エンコードされた出力の末尾にパディング文字 = が追加されます。具体的には、元のデータが3バイトの倍数でない場合、エンコードされた出力は4文字のブロックで構成され、最後のブロックが完全な3バイトを表さない場合に = が1つまたは2つ追加されます。

デコード処理においては、このパディング文字の存在と位置が非常に重要です。デコーダは、パディング文字を適切に解釈し、それ以降に予期せぬデータ(trailing garbage)が存在しないことを確認する必要があります。もし不正なデータがパディングの後に続く場合、それは通常、入力が破損しているか、意図しないデータが含まれていることを示します。

このコミット以前の encoding/base64 パッケージの Encoding.Decode() メソッドには、特定の条件下でこの末尾の不正なデータを検出できないというバグが存在しました。具体的には、入力文字列が == で終わるべきところに、その後に3バイト未満の余分な文字が続く場合(例: AA==x)、デコーダは x を不正なデータとして認識せず、デコード処理を正常に完了させてしまう可能性がありました。これはセキュリティ上の問題や、データ整合性の問題を引き起こす可能性があります。例えば、意図しないデータがデコード結果に混入したり、不正な入力がエラーとして扱われずに処理されてしまうことで、後続の処理に予期せぬ影響を与える可能性がありました。

このバグは、Base64デコーダの堅牢性を高め、不正な入力に対する耐性を向上させるために修正される必要がありました。

前提知識の解説

Base64エンコーディング

Base64は、バイナリデータをASCII文字列の形式に変換するエンコーディング方式です。主に、テキストベースのプロトコル(例: 電子メールのMIME、HTTPのBasic認証)でバイナリデータを安全に転送するために使用されます。

  • エンコードの仕組み:
    • 入力のバイナリデータを3バイト(24ビット)のブロックに分割します。
    • 各3バイトブロックを4つの6ビットブロックに変換します。
    • 各6ビットブロックをBase64アルファベット(A-Z, a-z, 0-9, +, /)のいずれかの文字にマッピングします。
    • 出力は4文字のブロックで構成されます。
  • パディング:
    • 入力データのバイト数が3の倍数でない場合、最後のブロックが完全な3バイトにならないことがあります。
    • この場合、エンコードされた出力の末尾にパディング文字 = が追加されます。
    • 元のデータが1バイトの場合、2つの = が追加されます(例: A==)。
    • 元のデータが2バイトの場合、1つの = が追加されます(例: AA=)。
    • 元のデータが3バイトの倍数の場合、パディングは不要です。
  • デコードの仕組み:
    • エンコードされたBase64文字列を4文字のブロックに分割します。
    • 各Base64文字を対応する6ビット値に変換します。
    • 4つの6ビットブロック(24ビット)を結合し、元の3バイトのバイナリデータに再構成します。
    • パディング文字 = は、元のデータの不足バイト数を示します。デコーダはこれを利用して、正しいバイト数を復元します。

Base64デコーダの堅牢性

Base64デコーダは、単に有効なBase64文字列をデコードするだけでなく、不正な入力(無効な文字、不正なパディング、末尾の余分なデータなど)を適切に処理し、エラーを報告する堅牢性が必要です。特に、末尾の不正なデータ(trailing garbage)の検出は重要です。これは、入力ストリームの終端を正確に判断し、意図しないデータがデコード結果に混入するのを防ぐためです。

Go言語の encoding/base64 パッケージ

Go言語の encoding/base64 パッケージは、Base64エンコーディングとデコーディングの機能を提供します。Encoding 型は、標準のBase64エンコーディングやURLセーフなBase64エンコーディングなど、異なるBase64バリアントを表現します。Decode() メソッドは、エンコードされたバイトスライスをデコードし、デコードされたバイト数とエラーを返します。

技術的詳細

このコミットの技術的詳細は、encoding/base64 パッケージ内の decode メソッドにおけるパディング処理と末尾の不正データ検出ロジックの変更に集約されます。

変更前のコードでは、デコードループ内でパディング文字 = を検出した際に、残りの入力バイト数とパディングの期待値に基づいてチェックを行っていました。しかし、このチェックが不十分であり、特に == の後に続く3バイト未満の不正なデータを見落とす可能性がありました。

変更後のコードでは、パディング文字 = が検出された際の処理がより厳密になっています。

  1. j の範囲チェックの変更: 変更前: for j := 0; j < 4; { ... j++ } 変更後: for j := range dbuf { ... } これはループのイディオム的な変更であり、dbuf のインデックス j0 から 3 の範囲で安全にアクセスされることを保証します。

  2. パディング文字 = 検出時のロジック強化: 変更前:

    if in == '=' && j >= 2 && len(src) < 4 {
        if len(src)+j < 4-1 {
            // not enough padding
            return n, false, CorruptInputError(olen)
        }
        if len(src) > 0 && src[0] != '=' {
            // incorrect padding
            return n, false, CorruptInputError(olen - len(src) - 1)
        }
        // ...
    }
    

    変更後:

    if in == '=' {
        // We've reached the end and there's padding
        switch j {
        case 0, 1:
            // incorrect padding: '=' appeared too early (e.g., "=AAA", "A=AA")
            return n, false, CorruptInputError(olen - len(src) - 1)
        case 2:
            // "==" is expected, the first "=" is already consumed.
            if len(src) == 0 {
                // not enough padding (e.g., "AA=")
                return n, false, CorruptInputError(olen)
            }
            if src[0] != '=' {
                // incorrect padding (e.g., "AA=A")
                return n, false, CorruptInputError(olen - len(src) - 1)
            }
            src = src[1:] // Consume the second '='
        }
        if len(src) > 0 {
            // trailing garbage (e.g., "AA==x")
            return n, false, CorruptInputError(olen - len(src))
        }
        dlen, end = j, true
        break
    }
    
    • switch j によるパディング位置の厳密なチェック:
      • j0 または 1 の場合(つまり、最初の2文字で = が現れた場合)、これは不正なパディングと見なされ、CorruptInputError が返されます。Base64のパディングは常に最後の4文字ブロックの3番目または4番目の位置に現れるためです(例: AA==, AAA=)。
      • j2 の場合(つまり、3番目の文字で = が現れた場合)、これは == パディングの最初の = であると期待されます。この場合、次の文字が = であることを厳密にチェックします。もし次の文字が = でないか、入力がそこで終了している場合は、不正なパディングとしてエラーを返します。
    • if len(src) > 0 による末尾の不正データ検出の強化: パディング文字が適切に処理された後、len(src)0 より大きい場合、それは末尾に不正なデータ(trailing garbage)が存在することを示します。この場合、CorruptInputError を返して、入力が破損していることを明確に通知します。これが、AA==xx のようなケースを検出する主要な変更点です。変更前は、len(src) > 0 && src[0] != '=' のチェックが j >= 2 の条件内で行われており、特定のシナリオでこのチェックがスキップされる可能性がありました。新しいロジックでは、パディング処理が完了した後に残りの入力があるかどうかを無条件にチェックすることで、より包括的な検出を実現しています。

これらの変更により、Base64デコーダは、不正なパディングや末尾の不正なデータを含む入力に対して、より正確かつ堅牢にエラーを報告できるようになりました。

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

src/pkg/encoding/base64/base64.go

--- a/src/pkg/encoding/base64/base64.go
+++ b/src/pkg/encoding/base64/base64.go
@@ -224,21 +224,33 @@ func (enc *Encoding) decode(dst, src []byte) (n int, end bool, err error) {
 		var dbuf [4]byte
 		dlen := 4

-		for j := 0; j < 4; {
+		for j := range dbuf {
 			if len(src) == 0 {
 				return n, false, CorruptInputError(olen - len(src) - j)
 			}
 			in := src[0]
 			src = src[1:]
-			if in == '=' && j >= 2 && len(src) < 4 {
+			if in == '=' {
 				// We've reached the end and there's padding
-				if len(src)+j < 4-1 {
-					// not enough padding
-					return n, false, CorruptInputError(olen)
-				}
-				if len(src) > 0 && src[0] != '=' {
+				switch j {
+				case 0, 1:
 					// incorrect padding
 					return n, false, CorruptInputError(olen - len(src) - 1)
+				case 2:
+					// "==" is expected, the first "=" is already consumed.
+					if len(src) == 0 {
+						// not enough padding
+						return n, false, CorruptInputError(olen)
+					}
+					if src[0] != '=' {
+						// incorrect padding
+						return n, false, CorruptInputError(olen - len(src) - 1)
+					}
+					src = src[1:]
+				}
+				if len(src) > 0 {
+					// trailing garbage
+					return n, false, CorruptInputError(olen - len(src))
 				}
 				dlen, end = j, true
 				break
@@ -247,7 +259,6 @@ func (enc *Encoding) decode(dst, src []byte) (n int, end bool, err error) {
 			if dbuf[j] == 0xFF {
 				return n, false, CorruptInputError(olen - len(src) - 1)
 			}
-			j++
 		}

 		// Pack 4x 6-bit source blocks into 3 byte destination

src/pkg/encoding/base64/base64_test.go

--- a/src/pkg/encoding/base64/base64_test.go
+++ b/src/pkg/encoding/base64/base64_test.go
@@ -149,9 +149,13 @@ func TestDecodeCorrupt(t *testing.T) {
 	}{
 		{"", -1},
 		{"!!!!", 0},
+		{"====", 0},
 		{"x===", 1},
+		{"=AAA", 0},
+		{"A=AA", 1},
 		{"AA=A", 2},
-\t\t{\"AAA=AAAA\", 3},\n+\t\t{\"AA==A\", 4},\n+\t\t{\"AAA=AAAA\", 4},\
+		{"AAA=AAAA", 4},
 		{"AAAAA", 4},
 		{"AAAAAA", 4},
 		{"A=", 1},

コアとなるコードの解説

src/pkg/encoding/base64/base64.go の変更

  • for j := range dbuf: ループのイテレーション方法が変更され、dbuf の要素数(4)に基づいてループが回るようになりました。これにより、j のインクリメント (j++) がループ本体から削除されています。
  • if in == '=' ブロックの再構築:
    • 以前は if in == '=' && j >= 2 && len(src) < 4 という条件でパディング処理に入っていましたが、新しいコードでは if in == '=' だけでパディング処理のブロックに入ります。これにより、パディング文字 = が現れたらすぐにその後の処理で厳密なチェックを行うようになります。
    • switch j ステートメントの導入:
      • case 0, 1: j が0または1の場合に = が現れるのは不正です(例: =AAA, A=AA)。Base64のパディングは常に3番目または4番目の文字として現れるため、即座に CorruptInputError を返します。
      • case 2: j が2の場合に = が現れるのは、== パディングの最初の = である可能性が高いです。この場合、次の文字が = であることを確認します。もし次の文字が = でないか、入力がそこで終了している場合は、不正なパディングとしてエラーを返します。src = src[1:] で2番目の = を消費します。
    • if len(src) > 0 による末尾の不正データ検出: switch j ブロックの後に、if len(src) > 0 というチェックが追加されました。これは、パディング文字が適切に処理された後でも、まだ入力 src にデータが残っている場合に、それが末尾の不正なデータ(trailing garbage)であると判断し、CorruptInputError を返すためのものです。この変更が、AA==x のようなケースで x を検出する主要な修正点です。

これらの変更により、デコーダはパディング文字の出現位置と、パディング後の残りの入力の有無をより厳密にチェックするようになり、不正なBase64入力に対するエラー検出能力が向上しました。

src/pkg/encoding/base64/base64_test.go の変更

TestDecodeCorrupt 関数に、デコーダが正しくエラーを検出すべき新しいテストケースが追加されています。

  • {"====", 0}: 4つの = は不正な入力です。
  • {"=AAA", 0}: 最初の文字が = は不正です。
  • {"A=AA", 1}: 2番目の文字が = は不正です。
  • {"AA==A", 4}: == の後に A という不正なデータが続くケース。これがまさにコミットメッセージで言及されている AA==x のようなケースをテストします。
  • {"AAA=AAAA", 4}: AAA= の後に AAAA という不正なデータが続くケース。

これらのテストケースは、修正されたデコーダが、以前見落としていた特定の種類の不正な入力を正しく CorruptInputError として報告することを確認するために追加されました。

関連リンク

参考にした情報源リンク