[インデックス 17394] ファイルの概要
このコミットは、Go言語の標準ライブラリである text/template
パッケージにおける、HTML、JavaScript、URLクエリのエスケープ処理がポインタを正しく扱っていなかった問題を修正します。具体的には、テンプレートの通常の評価ロジックが適用する引数評価と間接参照(ポインタのデリファレンス)のルールを、エスケープ関数にも適用することで、一貫性と正確性を向上させています。
コミット
commit 1f661fc205440ccfb46b76a964f50a1259c928d8
Author: Rob Pike <r@golang.org>
Date: Tue Aug 27 13:29:07 2013 +1000
text/template: make the escapers for HTML etc. handle pointers correctly
Apply the same rules for argument evaluation and indirection that are
used by the regular evaluator.
Fixes #5802
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/13257043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1f661fc205440ccfb46b76a964f50a1259c928d8
元コミット内容
text/template
: HTMLなどのエスケープ処理がポインタを正しく扱えるようにする。
通常の評価器が使用する引数評価と間接参照の同じルールを適用する。
Fixes #5802
変更の背景
Goの text/template
パッケージは、テキストベースの出力を生成するためのテンプレートエンジンを提供します。このパッケージには、クロスサイトスクリプティング (XSS) などのセキュリティ脆弱性を防ぐために、HTML、JavaScript、URLクエリなどのコンテキストに応じて自動的に値をエスケープする機能が含まれています。
このコミット以前は、テンプレート内で直接変数を表示する場合(例: {{.MyValue}}
)と、エスケープ関数(例: {{html .MyValue}}
)にその変数を渡す場合とで、ポインタの間接参照(デリファレンス)の挙動に不整合がありました。具体的には、エスケープ関数にポインタが渡された場合、それが指す実際の値ではなく、ポインタ自体が文字列化されてエスケープされる可能性がありました。これは、開発者が期待する挙動と異なり、意図しない出力や、場合によってはセキュリティ上の問題を引き起こす可能性がありました。
この不整合を解消し、テンプレートエンジン全体で引数の評価とポインタの間接参照に関する一貫したルールを適用することが、この変更の背景にあります。
なお、コミットメッセージに記載されている Fixes #5802
については、公開されている golang/go
リポジトリのIssueトラッカーでは該当するIssueが見つかりませんでした。これは、内部的なIssue番号であるか、あるいは別のトラッカーで管理されていた可能性があります。
前提知識の解説
-
Goの
text/template
パッケージ: Go言語でテキストベースの出力を生成するためのテンプレートエンジンです。データ構造をテンプレートに渡し、その構造のフィールドやメソッドにアクセスして動的なコンテンツを生成できます。セキュリティのために、コンテキストに応じた自動エスケープ機能(HTML、JavaScript、URLクエリなど)を備えています。 -
ポインタ (Pointers) と間接参照 (Indirection): Goにおけるポインタは、変数のメモリアドレスを保持する変数です。間接参照とは、ポインタが指すメモリアドレスに格納されている実際の値にアクセスする操作(デリファレンス)を指します。例えば、
*int
型のポインタp
がある場合、*p
と書くことでp
が指すint
型の値を取得できます。 -
reflect
パッケージ: Goのreflect
パッケージは、実行時にプログラムの構造(型、値、メソッドなど)を検査・操作するための機能を提供します。reflect.Value
は、Goの任意の値を抽象的に表現する型で、その値の型、種類 (Kind)、ポインタが指す値などを取得できます。 -
fmt.Fprint
とfmt.Sprint
:fmt.Fprint(w io.Writer, a ...interface{}) (n int, err error)
: 指定されたio.Writer
(例:os.Stdout
やbytes.Buffer
) に引数a
を文字列としてフォーマットして書き込みます。fmt.Sprint(a ...interface{}) string
: 引数a
を文字列としてフォーマットし、その結果の文字列を返します。 これらの関数は、引数がポインタである場合、デフォルトでそのポインタが指す値を適切に表示しようとします(ただし、reflect
パッケージのような詳細な制御はできません)。
-
エスケープ処理 (Escaping): Webアプリケーションにおいて、ユーザー入力などの信頼できないデータをHTML、JavaScript、URLなどのコンテキストに埋め込む際に、特殊文字を無害な形式に変換する処理です。これにより、XSS攻撃などのセキュリティ脆弱性を防ぎます。
- HTMLエスケープ:
<
を<
に、>
を>
に、&
を&
に、"
を"
に変換するなど。 - JavaScriptエスケープ:
'
を\'
に、"
を\"
に、改行を\n
に変換するなど。 - URLクエリエスケープ: スペースを
%20
に、特殊文字を%xx
形式に変換するなど。
- HTMLエスケープ:
技術的詳細
このコミットの核心は、text/template
パッケージ内の値の評価とエスケープ処理のロジックを統一することにあります。
以前は、テンプレートの通常の評価パス(例: {{.Field}}
)では、reflect
パッケージを使用してポインタの間接参照を適切に処理し、ポインタが指す実際の値を表示していました。しかし、html
、js
、urlquery
といったエスケープ関数は、引数として渡された値がポインタである場合に、この間接参照のロジックを十分に適用していませんでした。その結果、ポインタそのものが文字列化されてエスケープされるという問題が発生していました。
この修正では、以下の2つの主要な変更が導入されました。
-
printableValue
関数の導入 (exec.go
): この新しいヘルパー関数printableValue(v reflect.Value) (interface{}, bool)
は、reflect.Value
型の入力v
を受け取り、fmt.Fprint
やfmt.Sprint
で安全に表示できるinterface{}
型の値と、それが表示可能かどうかを示すブール値を返します。reflect.Ptr
(ポインタ) 型の場合、indirect(v)
を呼び出してポインタが指す実際の値を取得します。これにより、ポインタのデリファレンスが適切に行われます。nil
値の場合、<no value>
という文字列を返します。reflect.Chan
やreflect.Func
のように、直接表示すべきでない型の場合、nil, false
を返して表示不可であることを示します。 この関数により、値の「表示可能性」と「間接参照の解決」に関するロジックが一元化されました。
-
evalArgs
関数の導入とエスケープ関数への適用 (funcs.go
):evalArgs(args []interface{}) string
という新しいヘルパー関数が導入されました。この関数は、HTMLEscaper
、JSEscaper
、URLQueryEscaper
といったエスケープ関数に渡される引数のリストを処理します。evalArgs
は、各引数に対してprintableValue
を呼び出します。これにより、エスケープ関数に渡される前に、すべての引数が適切に間接参照され、表示可能な形式に変換されます。- 変換された引数は、最終的に
fmt.Sprint
を使用して単一の文字列に結合されます。 この変更により、エスケープ関数が引数を評価する際に、テンプレートの通常の評価器と同じポインタ間接参照のルールが適用されるようになり、一貫性が保たれます。
これらの変更により、例えば {{html .MyStringPointer}}
のようなテンプレート式で、.MyStringPointer
が *string
型のポインタであっても、それが指す文字列が正しく取得され、エスケープされるようになります。
コアとなるコードの変更箇所
src/pkg/text/template/exec.go
--- a/src/pkg/text/template/exec.go
+++ b/src/pkg/text/template/exec.go
@@ -755,12 +755,21 @@ func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
// the template.
func (s *state) printValue(n parse.Node, v reflect.Value) {
s.at(n)
+ iface, ok := printableValue(v)
+ if !ok {
+ s.errorf("can't print %s of type %s", n, v.Type())
+ }
+ fmt.Fprint(s.wr, iface)
+}
+
+// printableValue returns the, possibly indirected, interface value inside v that
+// is best for a call to formatted printer.
+func printableValue(v reflect.Value) (interface{}, bool) {
if v.Kind() == reflect.Ptr {
v, _ = indirect(v) // fmt.Fprint handles nil.
}
if !v.IsValid() {
- fmt.Fprint(s.wr, "<no value>")
- return
+ return "<no value>", true
}
if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) {
@@ -769,11 +778,11 @@ func (s *state) printValue(n parse.Node, v reflect.Value) {
} else {
switch v.Kind() {
case reflect.Chan, reflect.Func:
- s.errorf("can't print %s of type %s", n, v.Type())
+ return nil, false
}
}
}
- fmt.Fprint(s.wr, v.Interface())
+ return v.Interface(), true
}
// Types to help sort the keys in a map for reproducible output.
src/pkg/text/template/funcs.go
--- a/src/pkg/text/template/funcs.go
+++ b/src/pkg/text/template/funcs.go
@@ -452,15 +452,7 @@ func HTMLEscapeString(s string) string {
// HTMLEscaper returns the escaped HTML equivalent of the textual
// representation of its arguments.
func HTMLEscaper(args ...interface{}) string {
- ok := false
- var s string
- if len(args) == 1 {
- s, ok = args[0].(string)
- }
- if !ok {
- s = fmt.Sprint(args...)
- }
- return HTMLEscapeString(s)
+ return HTMLEscapeString(evalArgs(args))
}
// JavaScript escaping.
@@ -545,26 +537,35 @@ func jsIsSpecial(r rune) bool {
// JSEscaper returns the escaped JavaScript equivalent of the textual
// representation of its arguments.
func JSEscaper(args ...interface{}) string {
- ok := false
- var s string
- if len(args) == 1 {
- s, ok = args[0].(string)
- }
- if !ok {
- s = fmt.Sprint(args...)\n
- }\n
- return JSEscapeString(s)
+ return JSEscapeString(evalArgs(args))
}
// URLQueryEscaper returns the escaped value of the textual representation of
// its arguments in a form suitable for embedding in a URL query.
func URLQueryEscaper(args ...interface{}) string {
- s, ok := "", false
+ return url.QueryEscape(evalArgs(args))
+}
+
+// evalArgs formats the list of arguments into a string. It is therefore equivalent to
+// fmt.Sprint(args...)
+// except that each argument is indirected (if a pointer), as required,\n
+// using the same rules as the default string evaluation during template
+// execution.
+func evalArgs(args []interface{}) string {
+ ok := false
+ var s string
+ // Fast path for simple common case.
if len(args) == 1 {
s, ok = args[0].(string)
}
if !ok {
+ for i, arg := range args {
+ a, ok := printableValue(reflect.ValueOf(arg))
+ if ok {
+ args[i] = a
+ } // else left fmt do its thing
+ }
s = fmt.Sprint(args...)
}
- return url.QueryEscape(s)
+ return s
}
src/pkg/text/template/exec_test.go
--- a/src/pkg/text/template/exec_test.go
+++ b/src/pkg/text/template/exec_test.go
@@ -57,6 +57,7 @@ type T struct {
Err error
// Pointers
PI *int
+ PS *string
PSI *[]int
NIL *int
// Function (not method)
@@ -125,6 +126,7 @@ var tVal = &T{
Str: bytes.NewBuffer([]byte("foozle")),
Err: errors.New("erroozle"),
PI: newInt(23),
+ PS: newString("a string"),
PSI: newIntSlice(21, 22, 23),
BinaryFunc: func(a, b string) string { return fmt.Sprintf("[%s=%s]", a, b) },
VariadicFunc: func(s ...string) string { return fmt.Sprint("<", strings.Join(s, "+"), ">") },
@@ -143,9 +145,11 @@ var iVal I = tVal
// Helpers for creation.
func newInt(n int) *int {
- p := new(int)
- *p = n
- return p
+ return &n
+}
+
+func newString(s string) *string {
+ return &s
}
func newIntSlice(n ...int) *[]int {
@@ -282,6 +286,7 @@ var execTests = []execTest{
// Pointers.
{"*int", "{{.PI}}", "23", tVal, true},
+ {"*string", "{{.PS}}", "a string", tVal, true},
{"*[]int", "{{.PSI}}", "[21 22 23]", tVal, true},
{"*[]int[1]", "{{index .PSI 1}}", "22", tVal, true},
{"NIL", "{{.NIL}}", "<nil>", tVal, true},
@@ -391,6 +396,7 @@ var execTests = []execTest{\n "<script>alert("XSS");</script>", nil, true},\n {"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,\n "<script>alert("XSS");</script>", nil, true},\n+ {"html", `{{html .PS}}`, "a string", tVal, true},\n \n // JavaScript.\n {"js", `{{js .}}`, `It\'d be nice.`, `It'd be nice.`, true},\n```
## コアとなるコードの解説
### `src/pkg/text/template/exec.go` の変更
* **`printableValue` 関数の追加**:
この関数は、`reflect.Value` を受け取り、それがポインタであればデリファレンスし、`nil` や表示不可能な型(チャネル、関数)を適切に処理して、`fmt.Fprint` で安全に表示できる `interface{}` 値を返します。これにより、値の「表示可能」な形式への変換ロジックがカプセル化され、再利用性が高まりました。
* **`printValue` 関数の変更**:
`printValue` は、テンプレート内で直接値を表示する際に呼び出される関数です。この変更により、`printValue` は直接 `fmt.Fprint` を呼び出すのではなく、新しく追加された `printableValue` を介して値を取得するようになりました。これにより、通常のテンプレート評価パスでもポインタの間接参照が正しく行われることが保証されます。
### `src/pkg/text/template/funcs.go` の変更
* **`evalArgs` 関数の追加**:
この関数は、`HTMLEscaper`、`JSEscaper`、`URLQueryEscaper` といったエスケープ関数に渡される可変長引数 `args ...interface{}` を処理するための中心的なロジックを提供します。
* 各引数 `arg` に対して `printableValue(reflect.ValueOf(arg))` を呼び出すことで、引数がポインタである場合にそのポインタが指す実際の値を取得します。
* 最終的に、これらの(デリファレンスされた可能性のある)引数を `fmt.Sprint` で結合し、単一の文字列として返します。
これにより、エスケープ関数が引数を評価する際に、テンプレートの通常の評価器と同じポインタ間接参照のルールが適用されるようになり、一貫性が保たれます。
* **エスケープ関数の簡素化**:
`HTMLEscaper`、`JSEscaper`、`URLQueryEscaper` の各関数は、引数の処理ロジックを `evalArgs` に委譲する形に簡素化されました。これにより、コードの重複が排除され、保守性が向上しました。
### `src/pkg/text/template/exec_test.go` の変更
* **ポインタ型文字列のテストケース追加**:
`T` 構造体に `*string` 型のフィールド `PS` が追加され、`newString` ヘルパー関数も導入されました。
これにより、`{{.PS}}` のように直接ポインタ型文字列を表示するテストと、`{{html .PS}}` のようにエスケープ関数にポインタ型文字列を渡すテストが追加されました。これらのテストは、ポインタの間接参照がエスケープ処理においても正しく行われることを検証します。
これらの変更により、`text/template` パッケージ全体で、値の評価とポインタの間接参照に関する挙動が一貫したものとなり、より堅牢で予測可能なテンプレート処理が実現されました。
## 関連リンク
* Go言語 `text/template` パッケージのドキュメント: [https://pkg.go.dev/text/template](https://pkg.go.dev/text/template)
* Go言語 `reflect` パッケージのドキュメント: [https://pkg.go.dev/reflect](https://pkg.go.dev/reflect)
* Go言語 `fmt` パッケージのドキュメント: [https://pkg.go.dev/fmt](https://pkg.go.dev/fmt)
## 参考にした情報源リンク
* Go言語の公式ドキュメント (上記リンク)
* コミットの差分情報 (GitHub)
* Go言語のポインタに関する一般的な知識
* Webセキュリティにおけるエスケープ処理の概念