[インデックス 10556] ファイルの概要
このコミットは、Go言語の標準ライブラリである html/template
パッケージを、新しいテンプレートAPIに合わせて更新するものです。主な変更点は、text/template
パッケージの Template
型の埋め込み(embedding)を排除し、html/template
が text/template
の内部構造に誤ってアクセスすることを防ぐことで、不変条件(invariants)を保護することにあります。これにより、より安全で堅牢なHTMLテンプレート処理が実現されます。
コミット
commit 07ee3cc741604136254499ccaf1e6c9d1bd868ff
Author: Rob Pike <r@golang.org>
Date: Wed Nov 30 17:42:18 2011 -0500
html/template: update to new template API
Not quite done yet but enough is here to review.
Embedding is eliminated so clients can't accidentally reach
methods of text/template.Template that would break the
invariants.
TODO later: Add and Clone are unimplemented.
TODO later: address issue 2349
R=golang-dev, r, rsc
CC=golang-dev
https://golang.org/cl/5434077
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/07ee3cc741604136254499ccaf1e6c9d1bd868ff
元コミット内容
html/template: update to new template API
Not quite done yet but enough is here to review.
Embedding is eliminated so clients can't accidentally reach
methods of text/template.Template that would break the
invariants.
TODO later: Add and Clone are unimplemented.
TODO later: address issue 2349
R=golang-dev, r, rsc
CC=golang-dev
https://golang.org/cl/5434077
変更の背景
Go言語の html/template
パッケージは、ウェブアプリケーションでHTMLコンテンツを安全に生成するために設計されています。これは、クロスサイトスクリプティング(XSS)などの脆弱性を防ぐために、自動エスケープ機能を提供します。このパッケージは、汎用的なテキストテンプレートエンジンである text/template
パッケージの上に構築されています。
このコミットの背景には、html/template
が text/template
の Template
型を構造体埋め込み(embedding)によって利用していたことによる潜在的な問題がありました。構造体埋め込みは、Goにおいて他の型のメソッドを自身の型に「継承」させる便利な方法ですが、この場合、html/template.Template
の利用者が意図せず text/template.Template
のメソッドを呼び出してしまう可能性がありました。これにより、html/template
が提供する自動エスケープの不変条件(例えば、すべての出力が適切にエスケープされていること)が破られ、セキュリティ上の脆弱性につながる恐れがありました。
このコミットは、この問題を解決するために、html/template.Template
から text/template.Template
の埋め込みを排除し、代わりに text/template.Template
のインスタンスをフィールドとして持つように変更することで、APIの安全性を高めることを目的としています。これにより、html/template
の利用者は、html/template
が提供する安全なAPIのみを使用するよう強制され、意図しないセキュリティリスクを回避できます。
また、コミットメッセージには「Not quite done yet」とあり、Add
と Clone
メソッドが未実装であること、そしてissue 2349への対応が残されていることが示されています。これは、このコミットが html/template
パッケージのAPI変更の初期段階であり、さらなる作業が予定されていることを示唆しています。
前提知識の解説
Go言語のテンプレートパッケージ (text/template
と html/template
)
text/template
: Go言語に組み込まれている汎用的なテキストテンプレートエンジンです。任意のテキスト形式の出力を生成するために使用できます。プレースホルダーや制御構造(条件分岐、ループなど)をサポートし、データ構造をテンプレートに渡してレンダリングすることができます。html/template
:text/template
の上に構築されたパッケージで、HTMLコンテンツの生成に特化しています。最も重要な機能は、クロスサイトスクリプティング(XSS)攻撃を防ぐための自動エスケープです。テンプレート内でユーザー提供のデータがHTMLとして解釈される可能性がある場合、html/template
は自動的にそのデータをエスケープし、安全な出力に変換します。これにより、開発者が手動でエスケープ処理を行う手間を省き、セキュリティ脆弱性のリスクを低減します。
Go言語の構造体埋め込み (Struct Embedding)
Go言語では、ある構造体の中に別の構造体を匿名フィールドとして含めることができます。これを「構造体埋め込み」と呼びます。埋め込まれた構造体のフィールドやメソッドは、外側の構造体のフィールドやメソッドであるかのように直接アクセスできます。
例:
type Inner struct {
Value int
}
func (i Inner) GetValue() int {
return i.Value
}
type Outer struct {
Inner // Inner構造体を埋め込み
Name string
}
func main() {
o := Outer{Inner: Inner{Value: 10}, Name: "test"}
fmt.Println(o.Value) // Innerのフィールドに直接アクセス
fmt.Println(o.GetValue()) // Innerのメソッドに直接アクセス
}
構造体埋め込みはコードの再利用性を高める強力な機能ですが、今回のケースのように、埋め込まれた型のメソッドが外側の型の不変条件を破る可能性がある場合には、意図しない動作を引き起こすリスクがあります。
不変条件 (Invariants)
ソフトウェア開発における不変条件とは、プログラムの実行中、特定の時点(例えば、メソッドの呼び出し前後やオブジェクトのライフサイクル全体)で常に真であると保証される条件のことです。html/template
の文脈では、「生成されるHTML出力は常に安全にエスケープされている」ということが重要な不変条件となります。
技術的詳細
このコミットの核心は、html/template.Template
型の定義変更と、それに伴う関連関数の修正です。
変更前:
type Set struct {
escaped map[string]bool
text.Set // text/template.Set を埋め込み
}
type Template struct {
escaped bool
*text.Template // text/template.Template を埋め込み
}
html/template.Template
は *text.Template
を直接埋め込んでいました。これにより、html/template.Template
のインスタンスを通じて、text/template.Template
の公開メソッド(例えば、エスケープ処理をバイパスする可能性のあるメソッド)にアクセスできてしまう可能性がありました。これは、html/template
が提供するセキュリティ保証を損なうリスクがありました。
変更後:
type Template struct {
escaped bool
// We could embed the text/template field, but it's safer not to because
// we need to keep our version of the name space and the underlying
// template's in sync.
text *text.Template // text/template.Template をフィールドとして持つ
// Templates are grouped by sharing the set, a pointer.
set *map[string]*Template
}
html/template.Template
は *text.Template
を埋め込む代わりに、text *text.Template
という名前付きフィールドとして持つようになりました。これにより、text.Template
のメソッドにアクセスするには明示的に t.text.Method()
のように記述する必要があり、意図しないメソッド呼び出しを防ぐことができます。
また、Set
型が削除され、テンプレートのグループ化は Template
型内の set *map[string]*Template
フィールドによって管理されるようになりました。これは、複数のテンプレートが同じ名前空間を共有し、互いに参照できるようにするための変更です。
この変更に伴い、以下の関数やメソッドが修正されました。
escape
/escapeSet
からescapeTemplates
への変更: テンプレートのエスケープ処理を行う関数が、Set
型に依存しないescapeTemplates
関数に統一されました。これは、Template
型が自身のset
フィールドを通じて関連するテンプレートを管理するようになったためです。Execute
/ExecuteTemplate
メソッドの変更: テンプレートの実行時に、エスケープ処理が適切に行われるように、内部でescapeTemplates
を呼び出すようになりました。また、text.Template
のExecute
メソッドを直接呼び出すのではなく、t.text.Execute
のようにフィールド経由で呼び出すように変更されました。Parse
メソッドの変更: テンプレートのパース処理も、text.Template
のパース結果をhtml/template.Template
の内部フィールドに適切に反映するように修正されました。特に、text.Template
がパース時に新しいテンプレートを生成した場合、それらをhtml/template.Template
の名前空間にも追加するロジックが追加されました。New
、Funcs
、Delims
、Lookup
などのメソッドの追加/修正:html/template.Template
がtext/template.Template
の機能をラップし、安全なAPIとして提供するためのメソッドが追加または修正されました。これにより、html/template
の利用者は、text/template
の詳細を意識することなく、安全なテンプレート操作を行うことができます。Add
とClone
の未実装化: コミットメッセージにもあるように、これらのメソッドは一時的に未実装とされました。これは、APIの変更に伴い、これらのメソッドの安全な実装が後回しにされたことを示唆しています。
これらの変更により、html/template
パッケージは text/template
の内部実装からより独立し、より堅牢で安全なHTMLテンプレートエンジンとしての役割を強化しました。
コアとなるコードの変更箇所
このコミットにおける主要な変更は、src/pkg/html/template/template.go
ファイルに集中しています。
-
Template
構造体の定義変更:--- a/src/pkg/html/template/template.go +++ b/src/pkg/html/template/template.go @@ -7,233 +7,224 @@ package template import ( "fmt" "io" + "io/ioutil" "path/filepath" "text/template" ) -// Set is a specialized template.Set that produces a safe HTML document -// fragment. -type Set struct { - escaped map[string]bool - text.Set -} - // Template is a specialized template.Template that produces a safe HTML // document fragment. type Template struct { escaped bool - *template.Template -} - -// Execute applies the named template to the specified data object, writing -// the output to wr. -func (s *Set) Execute(wr io.Writer, name string, data interface{}) error { - if !s.escaped[name] { - if err := escapeSet(&s.Set, name); err != nil { -+ // We could embed the text/template field, but it's safer not to because -+ // we need to keep our version of the name space and the underlying -+ // template's in sync. -+ text *template.Template -+ // Templates are grouped by sharing the set, a pointer. -+ set *map[string]*Template +}
Set
型が削除され、Template
型が*template.Template
を埋め込む代わりに、text *template.Template
フィールドとset *map[string]*Template
フィールドを持つようになりました。 -
ExecuteTemplate
メソッドの導入とExecute
メソッドの変更:--- a/src/pkg/html/template/template.go +++ b/src/pkg/html/template/template.go @@ -20,20 +20,20 @@ type Template struct { escaped bool - *template.Template -} - -// Execute applies the named template to the specified data object, writing -// the output to wr. -func (s *Set) Execute(wr io.Writer, name string, data interface{}) error { - if !s.escaped[name] { - if err := escapeSet(&s.Set, name); err != nil { -+ text *template.Template -+ // Templates are grouped by sharing the set, a pointer. -+ set *map[string]*Template +} + +// ExecuteTemplate applies the template associated with t that has the given name +// to the specified data object and writes the output to wr. +func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error { + tmpl := t.Lookup(name) + if tmpl == nil { + return fmt.Errorf("template: no template %q associated with template %q", name, t.Name()) + } + if !tmpl.escaped { + if err := escapeTemplates(tmpl, name); err != nil { // TODO: make a method of set? + return err + } - if s.escaped == nil { - s.escaped = make(map[string]bool) - } - s.escaped[name] = true + } - return s.Set.Execute(wr, name, data) + return tmpl.text.ExecuteTemplate(wr, name, data) } // Parse parses a string into a set of named templates. Parse may be called @@ -41,20 +41,20 @@ func (s *Set) Execute(wr io.Writer, name string, data interface{}) error { // to the set. If a template is redefined, the element in the set is // overwritten with the new definition. -func (set *Set) Parse(src string) (*Set, error) { - set.escaped = nil - s, err := set.Set.Parse(src) +func (t *Template) Parse(src string) (*Template, error) { + t.escaped = false + ret, err := t.text.Parse(src) if err != nil { return nil, err } - if s != &(set.Set) { - panic("allocated new set") - } - return set, nil -} - -// Parse parses the template definition string to construct an internal -// representation of the template for execution. -func (tmpl *Template) Parse(src string) (*Template, error) { - tmpl.escaped = false - t, err := tmpl.Template.Parse(src) - if err != nil { - return nil, err + // In general, all the named templates might have changed underfoot. + // Regardless, some new ones may have been defined. + // The template.Template set has been updated; update ours. + for _, v := range ret.Templates() { + name := v.Name() + tmpl := t.Lookup(name) + if tmpl == nil { + tmpl = t.New(name) + } + tmpl.escaped = false + tmpl.text = v } - tmpl.Template = t - return tmpl, nil + return t, nil } // Execute applies a parsed template to the specified data object, @@ -62,10 +62,10 @@ func (tmpl *Template) Parse(src string) (*Template, error) { // writing the output to wr. func (t *Template) Execute(wr io.Writer, data interface{}) error { if !t.escaped { - if err := escape(t.Template); err != nil { + if err := escapeTemplates(t, t.Name()); err != nil { return err } t.escaped = true } - return t.Template.Execute(wr, data) + return t.text.Execute(wr, data) }
Set.Execute
が削除され、Template.ExecuteTemplate
が導入されました。Template.Execute
も内部でescapeTemplates
を呼び出すように変更されました。 -
New
関数の変更:--- a/src/pkg/html/template/template.go +++ b/src/pkg/html/template/template.go @@ -73,7 +73,13 @@ func (t *Template) Execute(wr io.Writer, data interface{}) error { // New allocates a new HTML template with the given name. func New(name string) *Template { - return &Template{false, template.New(name)} + set := make(map[string]*Template) + tmpl := &Template{ + false, + template.New(name), + &set, + } + (*tmpl.set)[name] = tmpl + return tmpl }
New
関数が、新しいTemplate
インスタンスを作成する際に、内部のtext.Template
と、テンプレートのグループを管理するためのset
マップを初期化するように変更されました。 -
ParseFiles
およびParseGlob
の実装変更:ParseFiles
とParseGlob
は、内部でioutil.ReadFile
を使用してファイルの内容を読み込み、それをTemplate.Parse
メソッドに渡すように変更されました。これにより、html/template
がtext/template
のファイルパース機能に直接依存するのではなく、独自の安全なパースフローを持つようになりました。
これらの変更は、html/template
が text/template
の内部実装から分離され、より独立した安全なAPIを提供するように再設計されたことを明確に示しています。
コアとなるコードの解説
このコミットの最も重要な変更は、html/template.Template
構造体から text/template.Template
の埋め込みを削除し、代わりに text *text.Template
という名前付きフィールドとして持つようにした点です。
なぜこの変更が重要なのか?
-
不変条件の保護:
html/template
の主要な目的は、HTML出力を自動的にエスケープすることでXSS攻撃を防ぐことです。text/template.Template
を埋め込んでいると、開発者が誤ってtext/template.Template
のメソッド(例えば、エスケープ処理を行わないExecute
メソッドなど)を呼び出してしまう可能性があります。これにより、html/template
が保証する「すべての出力は安全にエスケープされている」という不変条件が破られ、セキュリティ上の脆弱性が生じる恐れがありました。名前付きフィールドにすることで、t.text.Execute()
のように明示的にアクセスする必要があるため、誤った使用を防ぎやすくなります。 -
APIの明確化と制御:
html/template
は、text/template
の上にセキュリティ層を追加したものです。埋め込みを排除することで、html/template
はtext/template
の機能をより細かく制御できるようになります。html/template
は、text.Template
の特定のメソッドのみをラップし、必要に応じて追加のセキュリティチェックやエスケープ処理を適用できます。これにより、html/template
のAPIがより明確になり、開発者は安全な操作のみを行うよう誘導されます。 -
内部状態の同期: コミットメッセージにもあるように、「we need to keep our version of the name space and the underlying template's in sync.」という課題がありました。
text/template
が内部で管理するテンプレートの名前空間と、html/template
が管理する名前空間を同期させる必要がありました。埋め込みではなくフィールドとして持つことで、html/template
はtext.Template
の状態変化(例えば、新しいテンプレートがパースされた場合)をより明示的に検知し、自身の内部状態(set
マップなど)を更新できるようになります。
set *map[string]*Template
フィールドの役割:
この新しい set
フィールドは、html/template
パッケージ内で複数の Template
インスタンスが互いに参照し合うためのメカニズムを提供します。text/template
も内部でテンプレートの名前空間を管理していますが、html/template
は独自のセキュリティ要件(エスケープ状態など)を持つため、独自のテンプレート管理が必要です。set
フィールドは、同じグループに属するすべての html/template.Template
インスタンスが共有するマップへのポインタであり、これにより {{template "name"}}
のようなアクションで他のテンプレートを参照できるようになります。
escapeTemplates
関数の重要性:
escapeTemplates
関数は、テンプレートが実行される前に、そのテンプレートとそれに依存するすべてのテンプレートが適切にエスケープされることを保証する役割を担っています。この関数は、テンプレートの構文木を走査し、各アクション(例えば、{{.Var}}
や {{.Func}}
)の出力コンテキスト(HTML属性、JavaScript、CSSなど)を分析し、必要に応じて適切なエスケープ関数を挿入します。このコミットでは、このエスケープ処理が Set
型から独立し、Template
型のメソッドとして呼び出されるように変更されました。
これらの変更は、Go言語の html/template
パッケージが、セキュリティと堅牢性をさらに向上させるための重要なステップであったことを示しています。
関連リンク
- Go言語の
text/template
パッケージ公式ドキュメント: https://pkg.go.dev/text/template - Go言語の
html/template
パッケージ公式ドキュメント: https://pkg.go.dev/html/template - Go言語の構造体埋め込みに関する解説 (Go by Example): https://gobyexample.com/struct-embedding
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/5434077
はGerritの変更リストへのリンクです) - Go言語のIssue Tracker: https://github.com/golang/go/issues (コミットメッセージに記載されている
issue 2349
を検索することで、関連する議論や背景をさらに深く理解できます) - Go言語のテンプレートに関するブログ記事やチュートリアル (一般的な知識として):
- A Guide to Go's
html/template
Package: https://www.alexedwards.net/blog/a-guide-to-go-html-template - Go Templates: https://www.digitalocean.com/community/tutorials/how-to-use-go-templates
- Go HTML Templates and XSS: https://www.calhoun.io/go-html-templates-and-xss/
- A Guide to Go's
[インデックス 10556] ファイルの概要
このコミットは、Go言語の標準ライブラリである html/template
パッケージを、新しいテンプレートAPIに合わせて更新するものです。主な変更点は、text/template
パッケージの Template
型の埋め込み(embedding)を排除し、html/template
が text/template
の内部構造に誤ってアクセスすることを防ぐことで、不変条件(invariants)を保護することにあります。これにより、より安全で堅牢なHTMLテンプレート処理が実現されます。
コミット
commit 07ee3cc741604136254499ccaf1e6c9d1bd868ff
Author: Rob Pike <r@golang.org>
Date: Wed Nov 30 17:42:18 2011 -0500
html/template: update to new template API
Not quite done yet but enough is here to review.
Embedding is eliminated so clients can't accidentally reach
methods of text/template.Template that would break the
invariants.
TODO later: Add and Clone are unimplemented.
TODO later: address issue 2349
R=golang-dev, r, rsc
CC=golang-dev
https://golang.org/cl/5434077
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/07ee3cc741604136254499ccaf1e6c9d1bd868ff
元コミット内容
html/template: update to new template API
Not quite done yet but enough is here to review.
Embedding is eliminated so clients can't accidentally reach
methods of text/template.Template that would break the
invariants.
TODO later: Add and Clone are unimplemented.
TODO later: address issue 2349
R=golang-dev, r, rsc
CC=golang-dev
https://golang.org/cl/5434077
変更の背景
Go言語の html/template
パッケージは、ウェブアプリケーションでHTMLコンテンツを安全に生成するために設計されています。これは、クロスサイトスクリプティング(XSS)などの脆弱性を防ぐために、自動エスケープ機能を提供します。このパッケージは、汎用的なテキストテンプレートエンジンである text/template
パッケージの上に構築されています。
このコミットの背景には、html/template
が text/template
の Template
型を構造体埋め込み(embedding)によって利用していたことによる潜在的な問題がありました。構造体埋め込みは、Goにおいて他の型のメソッドを自身の型に「継承」させる便利な方法ですが、この場合、html/template.Template
の利用者が意図せず text/template.Template
のメソッドを呼び出してしまう可能性がありました。これにより、html/template
が提供する自動エスケープの不変条件(例えば、すべての出力が適切にエスケープされていること)が破られ、セキュリティ上の脆弱性につながる恐れがありました。
このコミットは、この問題を解決するために、html/template.Template
から text/template.Template
の埋め込みを排除し、代わりに text/template.Template
のインスタンスをフィールドとして持つように変更することで、APIの安全性を高めることを目的としています。これにより、html/template
の利用者は、html/template
が提供する安全なAPIのみを使用するよう強制され、意図しないセキュリティリスクを回避できます。
また、コミットメッセージには「Not quite done yet」とあり、Add
と Clone
メソッドが未実装であること、そしてissue 2349への対応が残されていることが示されています。これは、このコミットが html/template
パッケージのAPI変更の初期段階であり、さらなる作業が予定されていることを示唆しています。
前提知識の解説
Go言語のテンプレートパッケージ (text/template
と html/template
)
text/template
: Go言語に組み込まれている汎用的なテキストテンプレートエンジンです。任意のテキスト形式の出力を生成するために使用できます。プレースホルダーや制御構造(条件分岐、ループなど)をサポートし、データ構造をテンプレートに渡してレンダリングすることができます。html/template
:text/template
の上に構築されたパッケージで、HTMLコンテンツの生成に特化しています。最も重要な機能は、クロスサイトスクリプティング(XSS)攻撃を防ぐための自動エスケープです。テンプレート内でユーザー提供のデータがHTMLとして解釈される可能性がある場合、html/template
は自動的にそのデータをエスケープし、安全な出力に変換します。これにより、開発者が手動でエスケープ処理を行う手間を省き、セキュリティ脆弱性のリスクを低減します。
Go言語の構造体埋め込み (Struct Embedding)
Go言語では、ある構造体の中に別の構造体を匿名フィールドとして含めることができます。これを「構造体埋め込み」と呼びます。埋め込まれた構造体のフィールドやメソッドは、外側の構造体のフィールドやメソッドであるかのように直接アクセスできます。
例:
type Inner struct {
Value int
}
func (i Inner) GetValue() int {
return i.Value
}
type Outer struct {
Inner // Inner構造体を埋め込み
Name string
}
func main() {
o := Outer{Inner: Inner{Value: 10}, Name: "test"}
fmt.Println(o.Value) // Innerのフィールドに直接アクセス
fmt.Println(o.GetValue()) // Innerのメソッドに直接アクセス
}
構造体埋め込みはコードの再利用性を高める強力な機能ですが、今回のケースのように、埋め込まれた型のメソッドが外側の型の不変条件を破る可能性がある場合には、意図しない動作を引き起こすリスクがあります。
不変条件 (Invariants)
ソフトウェア開発における不変条件とは、プログラムの実行中、特定の時点(例えば、メソッドの呼び出し前後やオブジェクトのライフサイクル全体)で常に真であると保証される条件のことです。html/template
の文脈では、「生成されるHTML出力は常に安全にエスケープされている」ということが重要な不変条件となります。
技術的詳細
このコミットの核心は、html/template.Template
型の定義変更と、それに伴う関連関数の修正です。
変更前:
type Set struct {
escaped map[string]bool
text.Set // text/template.Set を埋め込み
}
type Template struct {
escaped bool
*text.Template // text/template.Template を埋め込み
}
html/template.Template
は *text.Template
を直接埋め込んでいました。これにより、html/template.Template
のインスタンスを通じて、text/template.Template
の公開メソッド(例えば、エスケープ処理をバイパスする可能性のあるメソッド)にアクセスできてしまう可能性がありました。これは、html/template
が提供するセキュリティ保証を損なうリスクがありました。
変更後:
type Template struct {
escaped bool
// We could embed the text/template field, but it's safer not to because
// we need to keep our version of the name space and the underlying
// template's in sync.
text *text.Template // text/template.Template をフィールドとして持つ
// Templates are grouped by sharing the set, a pointer.
set *map[string]*Template
}
html/template.Template
は *text.Template
を埋め込む代わりに、text *text.Template
という名前付きフィールドとして持つようになりました。これにより、text.Template
のメソッドにアクセスするには明示的に t.text.Method()
のように記述する必要があり、意図しないメソッド呼び出しを防ぐことができます。
また、Set
型が削除され、テンプレートのグループ化は Template
型内の set *map[string]*Template
フィールドによって管理されるようになりました。これは、複数のテンプレートが同じ名前空間を共有し、互いに参照できるようにするための変更です。
この変更に伴い、以下の関数やメソッドが修正されました。
escape
/escapeSet
からescapeTemplates
へ変更: テンプレートのエスケープ処理を行う関数が、Set
型に依存しないescapeTemplates
関数に統一されました。これは、Template
型が自身のset
フィールドを通じて関連するテンプレートを管理するようになったためです。Execute
/ExecuteTemplate
メソッドの変更: テンプレートの実行時に、エスケープ処理が適切に行われるように、内部でescapeTemplates
を呼び出すようになりました。また、text.Template
のExecute
メソッドを直接呼び出すのではなく、t.text.Execute
のようにフィールド経由で呼び出すように変更されました。Parse
メソッドの変更: テンプレートのパース処理も、text.Template
のパース結果をhtml/template.Template
の内部フィールドに適切に反映するように修正されました。特に、text.Template
がパース時に新しいテンプレートを生成した場合、それらをhtml/template.Template
の名前空間にも追加するロジックが追加されました。New
、Funcs
、Delims
、Lookup
などのメソッドの追加/修正:html/template.Template
がtext/template.Template
の機能をラップし、安全なAPIとして提供するためのメソッドが追加または修正されました。これにより、html/template
の利用者は、text/template
の詳細を意識することなく、安全なテンプレート操作を行うことができます。Add
とClone
の未実装化: コミットメッセージにもあるように、これらのメソッドは一時的に未実装とされました。これは、APIの変更に伴い、これらのメソッドの安全な実装が後回しにされたことを示唆しています。
これらの変更により、html/template
パッケージは text/template
の内部実装からより独立し、より堅牢で安全なHTMLテンプレートエンジンとしての役割を強化しました。
コアとなるコードの変更箇所
このコミットにおける主要な変更は、src/pkg/html/template/template.go
ファイルに集中しています。
-
Template
構造体の定義変更:--- a/src/pkg/html/template/template.go +++ b/src/pkg/html/template/template.go @@ -7,233 +7,224 @@ package template import ( "fmt" "io" + "io/ioutil" "path/filepath" "text/template" ) -// Set is a specialized template.Set that produces a safe HTML document -// fragment. -type Set struct { - escaped map[string]bool - text.Set -} - // Template is a specialized template.Template that produces a safe HTML // document fragment. type Template struct { escaped bool - *template.Template -} - -// Execute applies the named template to the specified data object, writing -// the output to wr. -func (s *Set) Execute(wr io.Writer, name string, data interface{}) error { - if !s.escaped[name] { - if err := escapeSet(&s.Set, name); err != nil { -+ // We could embed the text/template field, but it's safer not to because -+ // we need to keep our version of the name space and the underlying -+ // template's in sync. -+ text *template.Template -+ // Templates are grouped by sharing the set, a pointer. -+ set *map[string]*Template +}
Set
型が削除され、Template
型が*template.Template
を埋め込む代わりに、text *template.Template
フィールドとset *map[string]*Template
フィールドを持つようになりました。 -
ExecuteTemplate
メソッドの導入とExecute
メソッドの変更:--- a/src/pkg/html/template/template.go +++ b/src/pkg/html/template/template.go @@ -20,20 +20,20 @@ type Template struct { escaped bool - *template.Template -} - -// Execute applies the named template to the specified data object, writing -// the output to wr. -func (s *Set) Execute(wr io.Writer, name string, data interface{}) error { - if !s.escaped[name] { - if err := escapeSet(&s.Set, name); err != nil { -+ text *template.Template -+ // Templates are grouped by sharing the set, a pointer. -+ set *map[string]*Template +} + +// ExecuteTemplate applies the template associated with t that has the given name +// to the specified data object and writes the output to wr. +func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error { + tmpl := t.Lookup(name) + if tmpl == nil { + return fmt.Errorf("template: no template %q associated with template %q", name, t.Name()) + } + if !tmpl.escaped { + if err := escapeTemplates(tmpl, name); err != nil { // TODO: make a method of set? + return err + } - if s.escaped == nil { - s.escaped = make(map[string]bool) - } - s.escaped[name] = true + } - return s.Set.Execute(wr, name, data) + return tmpl.text.ExecuteTemplate(wr, name, data) } // Parse parses a string into a set of named templates. Parse may be called @@ -41,20 +41,20 @@ func (s *Set) Execute(wr io.Writer, name string, data interface{}) error { // to the set. If a template is redefined, the element in the set is // overwritten with the new definition. -func (set *Set) Parse(src string) (*Set, error) { - set.escaped = nil - s, err := set.Set.Parse(src) +func (t *Template) Parse(src string) (*Template, error) { + t.escaped = false + ret, err := t.text.Parse(src) if err != nil { return nil, err } - if s != &(set.Set) { - panic("allocated new set") - } - return set, nil -} - -// Parse parses the template definition string to construct an internal -// representation of the template for execution. -func (tmpl *Template) Parse(src string) (*Template, error) { - tmpl.escaped = false - t, err := tmpl.Template.Parse(src) - if err != nil { - return nil, err + // In general, all the named templates might have changed underfoot. + // Regardless, some new ones may have been defined. + // The template.Template set has been updated; update ours. + for _, v := range ret.Templates() { + name := v.Name() + tmpl := t.Lookup(name) + if tmpl == nil { + tmpl = t.New(name) + } + tmpl.escaped = false + tmpl.text = v } - tmpl.Template = t - return tmpl, nil + return t, nil } // Execute applies a parsed template to the specified data object, @@ -62,10 +62,10 @@ func (tmpl *Template) Parse(src string) (*Template, error) { // writing the output to wr. func (t *Template) Execute(wr io.Writer, data interface{}) error { if !t.escaped { - if err := escape(t.Template); err != nil { + if err := escapeTemplates(t, t.Name()); err != nil { return err } t.escaped = true } - return t.Template.Execute(wr, data) + return t.text.Execute(wr, data) }
Set.Execute
が削除され、Template.ExecuteTemplate
が導入されました。Template.Execute
も内部でescapeTemplates
を呼び出すように変更されました。 -
New
関数の変更:--- a/src/pkg/html/template/template.go +++ b/src/pkg/html/template/template.go @@ -73,7 +73,13 @@ func (t *Template) Execute(wr io.Writer, data interface{}) error { // New allocates a new HTML template with the given name. func New(name string) *Template { - return &Template{false, template.New(name)} + set := make(map[string]*Template) + tmpl := &Template{ + false, + template.New(name), + &set, + } + (*tmpl.set)[name] = tmpl + return tmpl }
New
関数が、新しいTemplate
インスタンスを作成する際に、内部のtext.Template
と、テンプレートのグループを管理するためのset
マップを初期化するように変更されました。 -
ParseFiles
およびParseGlob
の実装変更:ParseFiles
とParseGlob
は、内部でioutil.ReadFile
を使用してファイルの内容を読み込み、それをTemplate.Parse
メソッドに渡すように変更されました。これにより、html/template
がtext/template
のファイルパース機能に直接依存するのではなく、独自の安全なパースフローを持つようになりました。
これらの変更は、html/template
が text/template
の内部実装から分離され、より独立した安全なAPIを提供するように再設計されたことを明確に示しています。
コアとなるコードの解説
このコミットの最も重要な変更は、html/template.Template
構造体から text/template.Template
の埋め込みを削除し、代わりに text *text.Template
という名前付きフィールドとして持つようにした点です。
なぜこの変更が重要なのか?
-
不変条件の保護:
html/template
の主要な目的は、HTML出力を自動的にエスケープすることでXSS攻撃を防ぐことです。text/template.Template
を埋め込んでいると、開発者が誤ってtext/template.Template
のメソッド(例えば、エスケープ処理を行わないExecute
メソッドなど)を呼び出してしまう可能性があります。これにより、html/template
が保証する「すべての出力は安全にエスケープされている」という不変条件が破られ、セキュリティ上の脆弱性が生じる恐れがありました。名前付きフィールドにすることで、t.text.Execute()
のように明示的にアクセスする必要があるため、誤った使用を防ぎやすくなります。 -
APIの明確化と制御:
html/template
は、text/template
の上にセキュリティ層を追加したものです。埋め込みを排除することで、html/template
はtext/template
の機能をより細かく制御できるようになります。html/template
は、text.Template
の特定のメソッドのみをラップし、必要に応じて追加のセキュリティチェックやエスケープ処理を適用できます。これにより、html/template
のAPIがより明確になり、開発者は安全な操作のみを行うよう誘導されます。 -
内部状態の同期: コミットメッセージにもあるように、「we need to keep our version of the name space and the underlying template's in sync.」という課題がありました。
text/template
が内部で管理するテンプレートの名前空間と、html/template
が管理する名前空間を同期させる必要がありました。埋め込みではなくフィールドとして持つことで、html/template
はtext.Template
の状態変化(例えば、新しいテンプレートがパースされた場合)をより明示的に検知し、自身の内部状態(set
マップなど)を更新できるようになります。
set *map[string]*Template
フィールドの役割:
この新しい set
フィールドは、html/template
パッケージ内で複数の Template
インスタンスが互いに参照し合うためのメカニズムを提供します。text/template
も内部でテンプレートの名前空間を管理していますが、html/template
は独自のセキュリティ要件(エスケープ状態など)を持つため、独自のテンプレート管理が必要です。set
フィールドは、同じグループに属するすべての html/template.Template
インスタンスが共有するマップへのポインタであり、これにより {{template "name"}}
のようなアクションで他のテンプレートを参照できるようになります。
escapeTemplates
関数の重要性:
escapeTemplates
関数は、テンプレートが実行される前に、そのテンプレートとそれに依存するすべてのテンプレートが適切にエスケープされることを保証する役割を担っています。この関数は、テンプレートの構文木を走査し、各アクション(例えば、{{.Var}}
や {{.Func}}
)の出力コンテキスト(HTML属性、JavaScript、CSSなど)を分析し、必要に応じて適切なエスケープ関数を挿入します。このコミットでは、このエスケープ処理が Set
型から独立し、Template
型のメソッドとして呼び出されるように変更されました。
これらの変更は、Go言語の html/template
パッケージが、セキュリティと堅牢性をさらに向上させるための重要なステップであったことを示しています。
関連リンク
- Go言語の
text/template
パッケージ公式ドキュメント: https://pkg.go.dev/text/template - Go言語の
html/template
パッケージ公式ドキュメント: https://pkg.go.dev/html/template - Go言語の構造体埋め込みに関する解説 (Go by Example): https://gobyexample.com/struct-embedding
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/5434077
はGerritの変更リストへのリンクです) - Go言語のIssue Tracker: https://github.com/golang/go/issues (コミットメッセージに記載されている
issue 2349
を検索することで、関連する議論や背景をさらに深く理解できます) - Go言語のテンプレートに関するブログ記事やチュートリアル (一般的な知識として):
- A Guide to Go's
html/template
Package: https://www.alexedwards.net/blog/a-guide-to-go-html-template - Go Templates: https://www.digitalocean.com/community/tutorials/how-to-use-go-templates
- Go HTML Templates and XSS: https://www.calhoun.io/go-html-templates-and-xss/
- A Guide to Go's