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

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

このコミットは、Go言語の標準ライブラリ html/template パッケージにおける DOCTYPE 宣言の解析ロジックを修正するものです。具体的には、DOCTYPE 宣言がケースインセンシティブ(大文字・小文字を区別しない)にチェックされるように変更されました。これにより、HTMLの仕様に準拠し、様々な大文字・小文字の組み合わせで記述された DOCTYPE 宣言が正しく認識されるようになります。

コミット

commit c05c3a9d1180e6d449049d9ed96c46b65837bb29
Author: Scott Lawrence <bytbox@gmail.com>
Date:   Fri Feb 24 11:32:33 2012 +1100

    html/template: make doctype check case-insensitive
    
    Fixes #3094.
    
    R=golang-dev, rsc, nigeltao
    CC=golang-dev
    https://golang.org/cl/5687065

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

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

元コミット内容

html/template: make doctype check case-insensitive

Fixes #3094.

変更の背景

この変更は、Go言語の html/template パッケージが DOCTYPE 宣言を処理する際に、大文字・小文字を厳密に区別してしまうという問題(Issue #3094)を解決するために行われました。HTMLの仕様では、DOCTYPE 宣言は大文字・小文字を区別しない(ケースインセンシティブ)と定められています。しかし、当時の html/template の実装では、例えば <!DOCTYPE html> は認識できても <!doctype html><!DoCtYpE hTmL> のような記述は正しく認識されず、エスケープ処理が意図しない形で適用されてしまう可能性がありました。

この問題は、テンプレートエンジンが生成するHTMLの正確性に関わるものであり、特にセキュリティ上の観点からも重要です。html/template パッケージは、クロスサイトスクリプティング(XSS)などの脆弱性からアプリケーションを保護するために、自動エスケープ機能を提供しています。DOCTYPE 宣言が正しく認識されないと、このエスケープ処理が適切に機能せず、セキュリティ上のリスクが生じる可能性がありました。

このコミットは、この不整合を解消し、html/template がHTMLの標準に完全に準拠するようにすることで、より堅牢で安全なウェブアプリケーション開発を支援することを目的としています。

前提知識の解説

html/template パッケージ

html/template はGo言語の標準ライブラリの一つで、HTMLテンプレートを安全に生成するためのパッケージです。このパッケージの主要な機能は、出力されるHTMLコンテンツに対して自動的にエスケープ処理を施すことで、クロスサイトスクリプティング(XSS)攻撃などのウェブ脆弱性を防ぐことにあります。開発者は、ユーザー入力やデータベースからのデータを直接HTMLに埋め込む際に、手動でエスケープ処理を行う必要がなく、安全なウェブページを構築できます。

DOCTYPE 宣言

DOCTYPE 宣言は、HTMLドキュメントの冒頭に記述される特別な命令で、そのドキュメントがどのHTMLまたはXHTMLのバージョンに準拠しているかをウェブブラウザに伝えます。例えば、<!DOCTYPE html> は、そのドキュメントがHTML5の仕様に準拠していることを示します。ブラウザは DOCTYPE 宣言を読み取ることで、ドキュメントを「標準モード」または「互換モード(Quirks Mode)」のどちらでレンダリングするかを決定します。標準モードでは、ブラウザは最新のWeb標準に厳密に従ってページをレンダリングし、互換モードでは古いブラウザの挙動を模倣します。DOCTYPE 宣言はHTMLの構文規則上、大文字・小文字を区別しない(ケースインセンシティブ)とされています。

ケースインセンシティブ(Case-Insensitive)

ケースインセンシティブとは、文字列の比較やマッチングにおいて、大文字と小文字を区別しないことを指します。例えば、「Hello」と「hello」はケースインセンシティブな比較では同じものと見なされます。HTMLの多くの要素名や属性名、そして DOCTYPE 宣言はケースインセンシティブに扱われるべきです。

bytes.HasPrefix

Go言語の bytes パッケージに含まれる関数で、func HasPrefix(s, prefix []byte) bool というシグネチャを持ちます。これは、バイトスライス s がバイトスライス prefix で始まるかどうかをチェックします。この関数は、厳密なバイト列の一致をチェックするため、デフォルトではケースセンシティブです。

bytes.ToUpper

Go言語の bytes パッケージに含まれる関数で、func ToUpper(s []byte) []byte というシグネチャを持ちます。これは、与えられたバイトスライス s 内のすべてのASCII文字を大文字に変換した新しいバイトスライスを返します。この関数を使用することで、ケースセンシティブな比較を行う前に、文字列を統一されたケース(例えばすべて大文字)に変換し、実質的にケースインセンシティブな比較を実現できます。

技術的詳細

このコミットの技術的な核心は、html/template パッケージ内の escape.go ファイルにおける DOCTYPE 宣言のチェック方法の変更にあります。

変更前は、s[j:] というバイトスライス(現在の走査位置から文字列の最後まで)が doctypeBytes<!DOCTYPE html> のバイト表現)と直接 bytes.HasPrefix で比較されていました。この bytes.HasPrefix はケースセンシティブな比較を行うため、<!DOCTYPE html> と完全に一致する形式でなければ DOCTYPE 宣言として認識されませんでした。

変更後は、bytes.ToUpper(s[j:]) が導入されました。これにより、s[j:] の内容がまずすべて大文字に変換されてから、doctypeBytesbytes.HasPrefix で比較されるようになりました。doctypeBytes 自体も <!DOCTYPE html> の大文字形式で定義されているため、入力された DOCTYPE 宣言が <!doctype html> であろうと <!DoCtYpE hTmL> であろうと、bytes.ToUpper によってすべて大文字に変換され、正しく <!DOCTYPE HTML> と比較されるようになります。

この修正により、html/template はHTMLの仕様に準拠し、DOCTYPE 宣言のケースインセンシティブな性質を正しく扱えるようになりました。これにより、テンプレートエンジンが生成するHTMLの堅牢性が向上し、意図しないエスケープ処理の適用を防ぐことができます。

また、escape_test.go に新しいテストケースが追加され、<!doCtYPE htMl> のような混合ケースの DOCTYPE 宣言が正しく処理されることを検証しています。これは、変更が期待通りに機能し、回帰バグがないことを保証するために非常に重要です。

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

--- a/src/pkg/html/template/escape.go
+++ b/src/pkg/html/template/escape.go
@@ -593,7 +593,7 @@ func (e *escaper) escapeText(c context, n *parse.TextNode) context {
 			for j := i; j < end; j++ {
-				if s[j] == '<' && !bytes.HasPrefix(s[j:], doctypeBytes) {
+				if s[j] == '<' && !bytes.HasPrefix(bytes.ToUpper(s[j:]), doctypeBytes) {
 					b.Write(s[written:j])
 					b.WriteString("&lt;")
 					written = j + 1
--- a/src/pkg/html/template/escape_test.go
+++ b/src/pkg/html/template/escape_test.go
@@ -431,6 +431,11 @@ func TestEscape(t *testing.T) {
 			"<!DOCTYPE html>Hello, World!",
 			"<!DOCTYPE html>Hello, World!",
 		},
+		{
+			"HTML doctype not case-insensitive",
+			"<!doCtYPE htMl>Hello, World!",
+			"<!doCtYPE htMl>Hello, World!",
+		},
 		{
 			"No doctype injection",
 			`<!{{"DOCTYPE"}}`,

コアとなるコードの解説

変更の中心は src/pkg/html/template/escape.go の以下の行です。

-				if s[j] == '<' && !bytes.HasPrefix(s[j:], doctypeBytes) {
+				if s[j] == '<' && !bytes.HasPrefix(bytes.ToUpper(s[j:]), doctypeBytes) {

この if 文は、HTMLコンテンツを走査中に < 文字を見つけた際に、それが DOCTYPE 宣言の開始であるかどうかをチェックしています。

  • s[j] == '<':現在の文字が < であることを確認します。
  • !bytes.HasPrefix(...):続く部分が DOCTYPE 宣言ではないことを確認します。もし DOCTYPE 宣言であれば、その部分はエスケープする必要がないため、! で否定しています。

変更前は bytes.HasPrefix(s[j:], doctypeBytes) となっており、s[j:](現在の位置から文字列の終わりまでの部分文字列)が doctypeBytes<!DOCTYPE html> のバイト表現)と完全に一致するかどうかをケースセンシティブに比較していました。

変更後は bytes.HasPrefix(bytes.ToUpper(s[j:]), doctypeBytes) となっています。ここで bytes.ToUpper(s[j:]) が追加されています。これは、s[j:] の内容をすべて大文字に変換した新しいバイトスライスを生成し、その結果に対して bytes.HasPrefix を適用します。これにより、元の文字列が <!doctype html> であろうと <!DoCtYpE hTmL> であろうと、bytes.ToUpper によって <!DOCTYPE HTML> に変換され、doctypeBytes との比較がケースインセンシティブに行われるようになります。

この修正により、html/template はHTMLの仕様に準拠し、DOCTYPE 宣言の様々な大文字・小文字の組み合わせを正しく認識できるようになりました。

また、src/pkg/html/template/escape_test.go に追加されたテストケースは、この変更が正しく機能することを検証しています。

		{
			"HTML doctype not case-insensitive",
			"<!doCtYPE htMl>Hello, World!",
			"<!doCtYPE htMl>Hello, World!",
		},

このテストケースは、入力文字列 <!doCtYPE htMl>Hello, World! が、html/template によって処理された後も <!doCtYPE htMl>Hello, World! のままであることを期待しています。つまり、<!doCtYPE htMl> が正しく DOCTYPE 宣言として認識され、その後の Hello, World! がエスケープされずにそのまま出力されることを確認しています。これは、DOCTYPE 宣言がケースインセンシティブに扱われるようになったことの直接的な検証となります。

関連リンク

参考にした情報源リンク