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

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

このコミットは、Go言語のexp/htmlパッケージにおいて、HTMLパーサーがプレーンテキスト、生テキスト(raw text)、およびRCDATA(Raw Character Data)内でヌルバイト(NUL bytes, \x00)を検出した場合の挙動を修正するものです。具体的には、これらのヌルバイトをUnicodeの置換文字であるU+FFFD(``)に変換するように変更されています。これにより、HTML5の仕様に準拠し、潜在的なセキュリティリスクを軽減します。

コミット

commit 55f0c8b2cddff16de2bf101ec997bf96813615d4
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Fri Jul 27 09:27:10 2012 +1000

    exp/html: replace NUL bytes in plaintext, raw text, and RCDATA
    
    If NUL bytes occur inside certain elements, convert them to U+FFFD
    replacement character.
    
    Pass 1 additional test.
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/6452047

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

https://github.com/golang/go/commit/55f0c8b2cddff16de2bf101ec997bf96813615d4

元コミット内容

exp/html: replace NUL bytes in plaintext, raw text, and RCDATA

If NUL bytes occur inside certain elements, convert them to U+FFFD
replacement character.

Pass 1 additional test.

R=nigeltao
CC=golang-dev
https://golang.org/cl/6452047

変更の背景

この変更の背景には、HTML5のパース仕様とセキュリティ上の考慮事項があります。HTML5の仕様では、入力ストリーム中にヌルバイト(U+0000)が出現した場合、それをU+FFFD(REPLACEMENT CHARACTER)に置き換えることが義務付けられています。これは、ヌルバイトがC言語などで文字列の終端を示すために使われることが多く、過去にはヌルバイトインジェクションなどのセキュリティ脆弱性の原因となることがあったためです。

特に、<plaintext><script><style><textarea><title>などの要素内では、テキストコンテンツが特殊な方法で扱われます。これらの要素内のコンテンツは、通常のHTMLエンティティのパースやエスケープ処理を受けない「生テキスト」として扱われるか、あるいは特定のルールに基づいてパースされます。このようなコンテキストでヌルバイトが適切に処理されないと、ブラウザやアプリケーションの挙動に予期せぬ影響を与えたり、クロスサイトスクリプティング(XSS)などの攻撃経路となる可能性がありました。

このコミットは、exp/htmlパッケージがHTML5の仕様に厳密に準拠し、ヌルバイトの適切な置換を行うことで、より堅牢で安全なHTMLパーサーを提供することを目的としています。コミットメッセージにある「Pass 1 additional test」は、この変更によってこれまで失敗していた特定のテストケースが成功するようになったことを示しており、ヌルバイトの処理が正しく行われるようになったことを裏付けています。

前提知識の解説

ヌルバイト(NUL byte, \x00, U+0000)

ヌルバイトは、ASCIIコードで0x00、UnicodeでU+0000に割り当てられている制御文字です。プログラミング、特にC言語のような低レベル言語では、文字列の終端を示すマーカーとして広く使われています。しかし、この特性が原因で、異なるシステムやアプリケーション間でデータをやり取りする際に、ヌルバイトの解釈の違いがセキュリティ上の問題を引き起こすことがあります。例えば、ファイルパスやURLにヌルバイトが含まれている場合、一部のシステムではヌルバイト以降の文字列が無視され、意図しないファイルやリソースにアクセスしてしまう「ヌルバイトインジェクション」という脆弱性につながる可能性があります。

HTML5におけるヌルバイトの扱い

HTML5のパース仕様では、ヌルバイトは「パースエラー」と見なされます。しかし、これは致命的なエラーではなく、パーサーは回復処理を行います。具体的には、入力ストリームでヌルバイトが検出された場合、それをU+FFFD(REPLACEMENT CHARACTER)に置き換えることが求められています。U+FFFDは、文字コード変換などで文字が正しく表現できない場合に表示される記号()です。この置換により、ヌルバイトが持つ潜在的な危険性を無効化し、一貫したパース結果を保証します。

HTMLのコンテンツモデルとテキストの種類

HTMLでは、要素の内容がどのように解釈されるかによって、いくつかのコンテンツモデルがあります。このコミットで言及されている「plaintext」「raw text」「RCDATA」は、特にテキストコンテンツのパースに関連するものです。

  • Plaintext: <plaintext>要素(非推奨)のように、要素の内容が一切パースされず、そのままのテキストとして扱われる場合。
  • Raw Text: <script><style>要素のように、要素の内容がHTMLとしてパースされず、スクリプトやスタイルシートのコードとしてそのまま扱われる場合。ただし、終了タグのシーケンス(例: </script>)は特別に認識されます。
  • RCDATA (Raw Character Data): <textarea><title>要素のように、要素の内容が文字データとして扱われる場合。この場合、HTMLエンティティ(例: &lt;)はデコードされますが、それ以外のHTMLタグはパースされず、そのままのテキストとして扱われます。

これらのテキストタイプでは、通常のHTML要素とは異なり、ヌルバイトがそのまま残ってしまう可能性があり、それが問題となるため、特別な処理が必要とされます。

技術的詳細

Go言語のexp/htmlパッケージは、HTML5の仕様に基づいてHTMLドキュメントをパースするためのライブラリです。このパッケージは、入力ストリームをトークンに分割し、それらのトークンを基にDOMツリーを構築します。

このコミットの変更は、Tokenizer構造体と、そのNext()およびText()メソッドに集中しています。

Tokenizerは、HTMLの入力ストリームを読み込み、トークンを生成する役割を担います。HTMLのパースはステートマシンとして実装されており、現在の状態(例:データ状態、タグ名状態、テキスト状態など)に基づいて次の文字をどのように処理するかが決定されます。

問題となるのは、特定のHTML要素(例:<plaintext>, <script>, <style>, <textarea>, <title>)の内部で、ヌルバイトが通常のテキストとして扱われてしまうことです。これらの要素のコンテンツは、HTMLの通常のパースルールとは異なる「生」のテキストとして扱われるため、ヌルバイトがそのまま残ってしまう可能性があります。

このコミットでは、TokenizerconvertNULという新しいフィールドが追加され、現在のトークンがヌルバイトを置換する必要があるかどうかを追跡します。そして、Next()メソッドでTextTokenが生成される際に、このconvertNULフラグが設定されます。最終的に、Text()メソッドがトークンのテキストデータを返す直前に、このフラグがtrueであれば、ヌルバイトをU+FFFDに置換する処理が実行されます。

これにより、パーサーはHTML5の仕様に準拠し、ヌルバイトを安全に処理できるようになります。

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

このコミットによる主要なコード変更は以下の2つのファイルにあります。

  1. src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log
  2. src/pkg/exp/html/token.go

src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log の変更

--- a/src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log
+++ b/src/pkg/exp/html/testlogs/plain-text-unsafe.dat.log
@@ -7,7 +7,7 @@ PASS "<html>\x00\n <frameset></frameset>"
 PASS "<html><select>\x00"
 PASS "\x00"
 PASS "<body>\x00"
-FAIL "<plaintext>\x00filler\x00text\x00"
+PASS "<plaintext>\x00filler\x00text\x00"
 FAIL "<svg><![CDATA[\x00filler\x00text\x00]]>"
 FAIL "<body><!\x00>"
 FAIL "<body><!\x00filler\x00text>"

この変更は、テストログファイルにおける特定のテストケースの結果を更新しています。以前はFAIL(失敗)だった<plaintext>\x00filler\x00text\x00というテストケースが、このコミットの変更によってPASS(成功)に変わっています。これは、<plaintext>要素内のヌルバイトが正しく処理されるようになったことを示しています。

src/pkg/exp/html/token.go の変更

--- a/src/pkg/exp/html/token.go
+++ b/src/pkg/exp/html/token.go
@@ -152,6 +152,9 @@ type Tokenizer struct {
 	rawTag string
 	// textIsRaw is whether the current text token's data is not escaped.
 	textIsRaw bool
+	// convertNUL is whether NUL bytes in the current token's data should
+	// be converted into \ufffd replacement characters.
+	convertNUL bool
 }
 
 // Err returns the error associated with the most recent ErrorToken token.
@@ -597,16 +600,19 @@ func (z *Tokenizer) Next() TokenType {
 			for z.err == nil {
 				z.readByte()
 			}
+			z.data.end = z.raw.end
 			z.textIsRaw = true
 		} else {
 			z.readRawOrRCDATA()
 		}
 		if z.data.end > z.data.start {
 			z.tt = TextToken
+			z.convertNUL = true
 			return z.tt
 		}
 	}
 	z.textIsRaw = false
+	z.convertNUL = false
 
 loop:
 	for {
@@ -731,6 +737,11 @@ func convertNewlines(s []byte) []byte {
 	return s
 }
 
+var (
+	nul         = []byte("\x00")
+	replacement = []byte("\ufffd")
+)
+
 // Text returns the unescaped text of a text, comment or doctype token. The
 // contents of the returned slice may change on the next call to Next.
 func (z *Tokenizer) Text() []byte {
@@ -740,6 +751,9 @@ func (z *Tokenizer) Text() []byte {\n 		z.data.start = z.raw.end\n 		z.data.end = z.raw.end\n 		s = convertNewlines(s)\n+		if z.convertNUL && bytes.Contains(s, nul) {\n+			s = bytes.Replace(s, nul, replacement, -1)\n+		}\n 		if !z.textIsRaw {\n 			s = unescape(s, false)\n 		}\

コアとなるコードの解説

Tokenizer構造体へのフィールド追加

type Tokenizer struct {
	// ... 既存のフィールド ...
	// convertNUL is whether NUL bytes in the current token's data should
	// be converted into \ufffd replacement characters.
	convertNUL bool
}

Tokenizer構造体にconvertNULというブール型のフィールドが追加されました。このフィールドは、現在処理中のトークンデータに含まれるヌルバイトをU+FFFDに変換する必要があるかどうかを示すフラグとして機能します。

Next()メソッドにおけるconvertNULの設定

Next()メソッドは、次のHTMLトークンを読み込み、そのタイプを決定する主要なロジックを含んでいます。

func (z *Tokenizer) Next() TokenType {
	// ... 既存のロジック ...

	// 特定の条件(例:<plaintext>要素のコンテンツを読み込む場合)
	// または readRawOrRCDATA() が呼び出された後
	if z.data.end > z.data.start { // テキストデータが取得できた場合
		z.tt = TextToken // トークンタイプをTextTokenに設定
		z.convertNUL = true // ここでconvertNULフラグをtrueに設定
		return z.tt
	}
	z.textIsRaw = false
	z.convertNUL = false // それ以外の場合はfalseにリセット

	// ... 既存のロジック ...
}

Next()メソッド内で、TextTokenが生成される特定のパス(特に、<plaintext>要素のコンテンツやreadRawOrRCDATA()によって読み込まれる生テキスト/RCDATAの場合)において、z.convertNUL = trueが設定されます。これは、これらのコンテキストでヌルバイトの置換が必要であることを示します。それ以外の通常のテキストトークンや他のトークンタイプの場合、convertNULfalseにリセットされます。

グローバル変数の追加

var (
	nul         = []byte("\x00")
	replacement = []byte("\ufffd")
)

ヌルバイトと置換文字のバイトスライスがグローバル変数として定義されました。これにより、bytes.Replace関数で使用する際に、毎回バイトスライスを生成するオーバーヘッドがなくなります。

Text()メソッドにおけるヌルバイトの置換処理

Text()メソッドは、TextTokenの実際のテキストコンテンツを返す役割を担います。

func (z *Tokenizer) Text() []byte {
	// ... 既存のロジック ...

	s := z.raw.bytes()[z.data.start:z.data.end] // トークンデータを取得
	s = convertNewlines(s) // 改行コードの変換

	if z.convertNUL && bytes.Contains(s, nul) {
		s = bytes.Replace(s, nul, replacement, -1)
	}

	if !z.textIsRaw {
		s = unescape(s, false) // HTMLエンティティのアンエスケープ
	}
	return s
}

Text()メソッドの冒頭で、z.convertNULtrueであり、かつ取得したテキストデータsにヌルバイト(nul)が含まれている場合にのみ、bytes.Replace(s, nul, replacement, -1)が呼び出されます。この関数は、s内のすべてのヌルバイトをreplacement(U+FFFD)に置換します。-1は、すべての出現箇所を置換することを意味します。

この変更により、特定のコンテキストで読み込まれたテキストデータ内のヌルバイトが、HTML5の仕様に従ってU+FFFDに変換されることが保証されます。

関連リンク

参考にした情報源リンク