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

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

このコミットは、Go言語の標準ライブラリである html/template パッケージを、新しいテンプレートAPIに合わせて更新するものです。主な変更点は、text/template パッケージの Template 型の埋め込み(embedding)を排除し、html/templatetext/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/templatetext/templateTemplate 型を構造体埋め込み(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」とあり、AddClone メソッドが未実装であること、そしてissue 2349への対応が残されていることが示されています。これは、このコミットが html/template パッケージのAPI変更の初期段階であり、さらなる作業が予定されていることを示唆しています。

前提知識の解説

Go言語のテンプレートパッケージ (text/templatehtml/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.TemplateExecute メソッドを直接呼び出すのではなく、t.text.Execute のようにフィールド経由で呼び出すように変更されました。
  • Parse メソッドの変更: テンプレートのパース処理も、text.Template のパース結果を html/template.Template の内部フィールドに適切に反映するように修正されました。特に、text.Template がパース時に新しいテンプレートを生成した場合、それらを html/template.Template の名前空間にも追加するロジックが追加されました。
  • NewFuncsDelimsLookup などのメソッドの追加/修正: html/template.Templatetext/template.Template の機能をラップし、安全なAPIとして提供するためのメソッドが追加または修正されました。これにより、html/template の利用者は、text/template の詳細を意識することなく、安全なテンプレート操作を行うことができます。
  • AddClone の未実装化: コミットメッセージにもあるように、これらのメソッドは一時的に未実装とされました。これは、APIの変更に伴い、これらのメソッドの安全な実装が後回しにされたことを示唆しています。

これらの変更により、html/template パッケージは text/template の内部実装からより独立し、より堅牢で安全なHTMLテンプレートエンジンとしての役割を強化しました。

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

このコミットにおける主要な変更は、src/pkg/html/template/template.go ファイルに集中しています。

  1. 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 フィールドを持つようになりました。

  2. 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 を呼び出すように変更されました。

  3. 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 マップを初期化するように変更されました。

  4. ParseFiles および ParseGlob の実装変更: ParseFilesParseGlob は、内部で ioutil.ReadFile を使用してファイルの内容を読み込み、それを Template.Parse メソッドに渡すように変更されました。これにより、html/templatetext/template のファイルパース機能に直接依存するのではなく、独自の安全なパースフローを持つようになりました。

これらの変更は、html/templatetext/template の内部実装から分離され、より独立した安全なAPIを提供するように再設計されたことを明確に示しています。

コアとなるコードの解説

このコミットの最も重要な変更は、html/template.Template 構造体から text/template.Template の埋め込みを削除し、代わりに text *text.Template という名前付きフィールドとして持つようにした点です。

なぜこの変更が重要なのか?

  1. 不変条件の保護: html/template の主要な目的は、HTML出力を自動的にエスケープすることでXSS攻撃を防ぐことです。text/template.Template を埋め込んでいると、開発者が誤って text/template.Template のメソッド(例えば、エスケープ処理を行わない Execute メソッドなど)を呼び出してしまう可能性があります。これにより、html/template が保証する「すべての出力は安全にエスケープされている」という不変条件が破られ、セキュリティ上の脆弱性が生じる恐れがありました。名前付きフィールドにすることで、t.text.Execute() のように明示的にアクセスする必要があるため、誤った使用を防ぎやすくなります。

  2. APIの明確化と制御: html/template は、text/template の上にセキュリティ層を追加したものです。埋め込みを排除することで、html/templatetext/template の機能をより細かく制御できるようになります。html/template は、text.Template の特定のメソッドのみをラップし、必要に応じて追加のセキュリティチェックやエスケープ処理を適用できます。これにより、html/template のAPIがより明確になり、開発者は安全な操作のみを行うよう誘導されます。

  3. 内部状態の同期: コミットメッセージにもあるように、「we need to keep our version of the name space and the underlying template's in sync.」という課題がありました。text/template が内部で管理するテンプレートの名前空間と、html/template が管理する名前空間を同期させる必要がありました。埋め込みではなくフィールドとして持つことで、html/templatetext.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 パッケージが、セキュリティと堅牢性をさらに向上させるための重要なステップであったことを示しています。

関連リンク

参考にした情報源リンク

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

このコミットは、Go言語の標準ライブラリである html/template パッケージを、新しいテンプレートAPIに合わせて更新するものです。主な変更点は、text/template パッケージの Template 型の埋め込み(embedding)を排除し、html/templatetext/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/templatetext/templateTemplate 型を構造体埋め込み(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」とあり、AddClone メソッドが未実装であること、そしてissue 2349への対応が残されていることが示されています。これは、このコミットが html/template パッケージのAPI変更の初期段階であり、さらなる作業が予定されていることを示唆しています。

前提知識の解説

Go言語のテンプレートパッケージ (text/templatehtml/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.TemplateExecute メソッドを直接呼び出すのではなく、t.text.Execute のようにフィールド経由で呼び出すように変更されました。
  • Parse メソッドの変更: テンプレートのパース処理も、text.Template のパース結果を html/template.Template の内部フィールドに適切に反映するように修正されました。特に、text.Template がパース時に新しいテンプレートを生成した場合、それらを html/template.Template の名前空間にも追加するロジックが追加されました。
  • NewFuncsDelimsLookup などのメソッドの追加/修正: html/template.Templatetext/template.Template の機能をラップし、安全なAPIとして提供するためのメソッドが追加または修正されました。これにより、html/template の利用者は、text/template の詳細を意識することなく、安全なテンプレート操作を行うことができます。
  • AddClone の未実装化: コミットメッセージにもあるように、これらのメソッドは一時的に未実装とされました。これは、APIの変更に伴い、これらのメソッドの安全な実装が後回しにされたことを示唆しています。

これらの変更により、html/template パッケージは text/template の内部実装からより独立し、より堅牢で安全なHTMLテンプレートエンジンとしての役割を強化しました。

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

このコミットにおける主要な変更は、src/pkg/html/template/template.go ファイルに集中しています。

  1. 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 フィールドを持つようになりました。

  2. 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 を呼び出すように変更されました。

  3. 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 マップを初期化するように変更されました。

  4. ParseFiles および ParseGlob の実装変更: ParseFilesParseGlob は、内部で ioutil.ReadFile を使用してファイルの内容を読み込み、それを Template.Parse メソッドに渡すように変更されました。これにより、html/templatetext/template のファイルパース機能に直接依存するのではなく、独自の安全なパースフローを持つようになりました。

これらの変更は、html/templatetext/template の内部実装から分離され、より独立した安全なAPIを提供するように再設計されたことを明確に示しています。

コアとなるコードの解説

このコミットの最も重要な変更は、html/template.Template 構造体から text/template.Template の埋め込みを削除し、代わりに text *text.Template という名前付きフィールドとして持つようにした点です。

なぜこの変更が重要なのか?

  1. 不変条件の保護: html/template の主要な目的は、HTML出力を自動的にエスケープすることでXSS攻撃を防ぐことです。text/template.Template を埋め込んでいると、開発者が誤って text/template.Template のメソッド(例えば、エスケープ処理を行わない Execute メソッドなど)を呼び出してしまう可能性があります。これにより、html/template が保証する「すべての出力は安全にエスケープされている」という不変条件が破られ、セキュリティ上の脆弱性が生じる恐れがありました。名前付きフィールドにすることで、t.text.Execute() のように明示的にアクセスする必要があるため、誤った使用を防ぎやすくなります。

  2. APIの明確化と制御: html/template は、text/template の上にセキュリティ層を追加したものです。埋め込みを排除することで、html/templatetext/template の機能をより細かく制御できるようになります。html/template は、text.Template の特定のメソッドのみをラップし、必要に応じて追加のセキュリティチェックやエスケープ処理を適用できます。これにより、html/template のAPIがより明確になり、開発者は安全な操作のみを行うよう誘導されます。

  3. 内部状態の同期: コミットメッセージにもあるように、「we need to keep our version of the name space and the underlying template's in sync.」という課題がありました。text/template が内部で管理するテンプレートの名前空間と、html/template が管理する名前空間を同期させる必要がありました。埋め込みではなくフィールドとして持つことで、html/templatetext.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 パッケージが、セキュリティと堅牢性をさらに向上させるための重要なステップであったことを示しています。

関連リンク

参考にした情報源リンク