[インデックス 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/templatePackage: 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/templatePackage: 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