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

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

このコミットは、Go言語の html/template パッケージにおける重要な変更を導入しています。具体的には、html/templatetext/template パッケージをラップする形に変更され、エスケープ処理がテンプレートの実行時に自動的に行われるようになりました。これにより、開発者が明示的に Escape 関数を呼び出す必要がなくなり、より安全で使いやすいテンプレートシステムが提供されます。

コミット

  • コミットハッシュ: a5291099d2a79c8cc85c331dfd08ad42e92ce063
  • Author: Mike Samuel mikesamuel@gmail.com
  • Date: Fri Nov 4 13:09:21 2011 -0400

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

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

元コミット内容

    html/template: wraps package template instead of exposing func Escape
    
    This does escaping on first execution.
    
    template.go defines the same interface elements as package template.
    It requires rather more duplication of code than I'd like, but I'm
    not clear how to avoid that.
    
    Maybe instead of
    
        mySet.ParseGlob(...)
        template.ParseSetGlob(...)
        mySet.ParseFiles(...)
        mySet.ParseTemplateFiles(...)
        template.ParseTemplateFiles(...)
    
    we combine these into a fileset abstraction that can be wrapped
    
        var fileset template.FileSet
        fileset.Glob(...)  // Load a few files by glob
        fileset.Files(...)  // Load a few {{define}}d files
        fileset.TemplateFiles(...)  // Load a few files as template bodies
        fileset.Funcs(...)  // Make the givens func available to templates
        // Do the parsing.
        set, err := fileset.ParseSet()
        // or set, err := fileset.ParseInto(set)
    
    or provide an interface that can receive filenames and functions and
    parse messages:
    
        type Bundle interface {
          TemplateFile(string)
          File(string)
          Funcs(FuncMap)
        }
    
    and define template.Parse* to handle the file-system stuff and send
    messages to a bundle:
    
        func ParseFiles(b Bundle, filenames ...string)
    
    R=r, r
    CC=golang-dev
    https://golang.org/cl/5270042

変更の背景

このコミットの主な背景は、Go言語の html/template パッケージの使いやすさと安全性を向上させることです。以前のバージョンでは、開発者はテンプレートをパースした後、明示的に Escape または EscapeSet 関数を呼び出してエスケープ処理を行う必要がありました。これは、開発者がエスケープ処理を忘れる可能性があり、結果としてクロスサイトスクリプティング(XSS)などのセキュリティ脆弱性を引き起こすリスクがありました。

この変更により、html/templatetext/template パッケージを内部的にラップし、テンプレートの実行時に自動的にコンテキストに応じたエスケープ処理を行うようになりました。これにより、開発者は html/templatetext/template と同じように扱うことができ、エスケープ処理を意識することなく安全なHTML出力を生成できるようになります。

コミットメッセージでは、コードの重複を避けるための代替案(fileset 抽象化や Bundle インターフェース)も議論されていますが、最終的には html/templatetext/template をラップする現在の実装が採用されました。これは、既存の text/template のAPIとの互換性を保ちつつ、セキュリティを強化するための現実的なアプローチとして選択されたと考えられます。

前提知識の解説

Go言語のテンプレートパッケージ (text/templatehtml/template)

Go言語には、テキストベースの出力を生成するための text/template パッケージと、HTML出力を安全に生成するための html/template パッケージがあります。

  • text/template: 任意のテキスト形式の出力を生成するための汎用的なテンプレートエンジンです。データがそのまま出力されるため、HTMLなどのマークアップ言語を生成する際には、手動でエスケープ処理を行う必要があります。
  • html/template: text/template をベースにしており、HTML出力に特化したセキュリティ機能を提供します。特に、**コンテキストに応じた自動エスケープ(Contextual Autoescaping)**が特徴です。

コンテキストに応じた自動エスケープ (Contextual Autoescaping)

コンテキストに応じた自動エスケープは、Webアプリケーションのセキュリティにおいて非常に重要な概念です。これは、テンプレート内で動的なデータが挿入される際に、そのデータが使用されるHTMLのコンテキスト(例: HTML要素のテキストコンテンツ、属性値、JavaScriptコード、CSSスタイル、URIなど)を自動的に判断し、適切なエスケープ処理を適用する仕組みです。

例えば、ユーザーが入力した文字列がHTMLの <p> タグ内に表示される場合と、JavaScriptの <script> タグ内に表示される場合では、必要なエスケープ処理が異なります。

  • HTMLコンテキスト: <&lt; に、>&gt; に、&&amp; に変換するなど。
  • JavaScriptコンテキスト: 文字列リテラル内で特殊文字をエスケープする(例: "\" に、\\\ に変換)など。
  • URIコンテキスト: URLエンコード(例: スペースを %20 に変換)など。

この自動エスケープ機能により、開発者が手動でエスケープ処理を行う手間が省けるだけでなく、エスケープ漏れによるクロスサイトスクリプティング(XSS)攻撃のリスクを大幅に低減できます。

クロスサイトスクリプティング (XSS)

クロスサイトスクリプティング(XSS)は、Webアプリケーションの脆弱性の一種で、攻撃者が悪意のあるスクリプト(通常はJavaScript)をWebページに注入し、そのスクリプトが他のユーザーのブラウザで実行されることを可能にします。これにより、攻撃者はセッションハイジャック、個人情報の窃取、Webサイトの改ざんなど、様々な悪意のある行為を行うことができます。

XSS攻撃は、主に以下のシナリオで発生します。

  1. 反射型XSS: 攻撃者が作成した悪意のあるURLをユーザーにクリックさせることで、そのURLに含まれるスクリプトがユーザーのブラウザで実行される。
  2. 格納型XSS: 攻撃者が悪意のあるスクリプトをWebアプリケーションのデータベースなどに保存し、他のユーザーがそのデータを含むページを閲覧した際にスクリプトが実行される。
  3. DOM Based XSS: クライアントサイドのJavaScriptが、ユーザーから提供されたデータを不適切に処理することで発生する。

html/template のコンテキストに応じた自動エスケープは、これらのXSS攻撃を防ぐための強力な防御メカニズムとして機能します。

技術的詳細

このコミットの技術的な核心は、html/template パッケージが text/template パッケージの機能を内部的に利用しつつ、エスケープ処理を透過的に行うように再設計された点にあります。

具体的には、以下の変更が行われました。

  1. html/template/template.go の新規追加:

    • SetTemplate という新しい型が定義されました。これらはそれぞれ text/template.Settext/template.Template をラップしています。
    • これらの新しい型には、Parse, Execute, New, Must, ParseFile, ParseFiles, ParseGlob などのメソッドが定義されており、text/template と同様のAPIを提供します。
    • 重要なのは、Execute メソッドが呼び出される際に、内部的に escape または escapeSet 関数が呼び出され、テンプレートのエスケープ処理が自動的に行われるようになったことです。これにより、開発者は明示的にエスケープ関数を呼び出す必要がなくなりました。
  2. Escape および EscapeSet 関数の内部化:

    • 以前は公開されていた html/template.Escape および html/template.EscapeSet 関数が、それぞれ escape および escapeSet という非公開関数に変更されました。
    • これらの関数は、引き続きテンプレートのAST(抽象構文木)を走査し、各ノードのコンテキストを分析して適切なエスケープ関数を挿入する役割を担います。しかし、これらの関数はもはや外部から直接呼び出されることはなく、html/templateExecute メソッドから透過的に呼び出されます。
  3. エラー処理の改善:

    • error.go 内のエラーメッセージが更新され、EscapeSet の代わりに html/template パッケージが静的に解析を行うことを示すように変更されました。
    • ErrNoNames エラーが削除されました。これは、EscapeSet が非公開になり、テンプレート名が常に内部的に処理されるようになったため、不要になったためです。
  4. テストコードの修正:

    • 既存のテストコードが、新しいAPI(html/template.New, html/template.Must, html/template.Set など)を使用するように修正されました。
    • EscapeEscapeSet の直接的な呼び出しが削除され、Execute メソッドを介した自動エスケープのテストに重点が置かれました。

この変更により、html/template はより「透過的」なセキュリティ層を提供するようになりました。開発者は text/template とほぼ同じ感覚でテンプレートを記述・実行でき、セキュリティ上の懸念は html/template が自動的に処理するという設計思想が明確になりました。

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

このコミットにおける最も重要な変更は、src/pkg/html/template/template.go の新規追加と、既存の escape.go および doc.go の変更です。

src/pkg/html/template/template.go (新規追加)

このファイルは、html/template パッケージの新しいエントリポイントとなり、text/template パッケージの機能をラップします。

// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package html

import (
	"fmt"
	"io"
	"path/filepath"
	"template" // text/template パッケージをインポート
)

// Set is a specialized template.Set that produces a safe HTML document
// fragment.
type Set struct {
	escaped map[string]bool
	template.Set // text/template.Set を埋め込み
}

// Template is a specialized template.Template that produces a safe HTML
// document fragment.
type Template struct {
	escaped bool
	*template.Template // text/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 { // 実行時にエスケープ処理を呼び出し
			return err
		}
		if s.escaped == nil {
			s.escaped = make(map[string]bool)
		}
		s.escaped[name] = true
	}
	return s.Set.Execute(wr, name, data) // text/template の Execute を呼び出し
}

// Parse parses a string into a set of named templates.
func (set *Set) Parse(src string) (*Set, error) {
	set.escaped = nil
	s, err := set.Set.Parse(src)
	if err != nil {
		return nil, err
	}
	if s != &(set.Set) {
		panic("allocated new set")
	}
	return set, nil
}

// ... (他のParse, New, Must, ParseFile, ParseGlobなどのメソッドが続く)

src/pkg/html/template/escape.go の変更

EscapeEscapeSet 関数が非公開 (escape, escapeSet) に変更されました。

--- a/src/pkg/html/template/escape.go
+++ b/src/pkg/html/template/escape.go
@@ -12,31 +12,23 @@ import (
 	"template/parse"
 )
 
-// Escape rewrites each action in the template to guarantee that the output is
+// escape rewrites each action in the template to guarantee that the output is
 // properly escaped.
-func Escape(t *template.Template) (*template.Template, error) {
+func escape(t *template.Template) error {
 	var s template.Set
 	s.Add(t)
-	if _, err := EscapeSet(&s, t.Name()); err != nil {
-		return nil, err
-	}
+	if err := escapeSet(&s, t.Name()); err != nil {
+		return err
+	}
 	// TODO: if s contains cloned dependencies due to self-recursion
 	// cross-context, error out.
-	return t, nil
+	return nil
 }
 
-// EscapeSet rewrites the template set to guarantee that the output of any of
+// escapeSet rewrites the template set to guarantee that the output of any of
 // the named templates is properly escaped.
 // Names should include the names of all templates that might be Executed but
 // need not include helper templates.
 // If no error is returned, then the named templates have been modified. 
 // Otherwise the named templates have been rendered unusable.
-func EscapeSet(s *template.Set, names ...string) (*template.Set, error) {
-	if len(names) == 0 {
-		// TODO: Maybe add a method to Set to enumerate template names
-		// and use those instead.
-		return nil, &Error{ErrNoNames, "", 0, "must specify names of top level templates"}
-	}
+func escapeSet(s *template.Set, names ...string) error {
 	e := newEscaper(s)
 	for _, name := range names {
 		c, _ := e.escapeTree(context{}, name, 0)
@@ -53,11 +45,11 @@ func EscapeSet(s *template.Set, names ...string) (*template.Set, error) {
 				t.Tree = nil
 				}
 			}
-			return nil, err
+			return err
 		}
 	}
 	e.commit()
-	return s, nil
+	return nil
 }

src/pkg/html/template/doc.go の変更

ドキュメントが更新され、html/templatetext/template をラップし、自動エスケープが透過的に行われることが明記されました。

--- a/src/pkg/html/template/doc.go
+++ b/src/pkg/html/template/doc.go
@@ -9,59 +9,54 @@ construction of HTML output that is safe against code injection.
 
 Introduction
 
--To use this package, invoke the standard template package to parse a template
--set, and then use this package’s EscapeSet function to secure the set.
--The arguments to EscapeSet are the template set and the names of all templates
--that will be passed to Execute.
-+This package wraps package template so you can use the standard template API
-+to parse and execute templates.
  
      set, err := new(template.Set).Parse(...)\n-    set, err = EscapeSet(set, "templateName0", ...)\n+    // Error checking elided\n+    err = set.Execute(out, "Foo", data)\n  
 -If successful, set will now be injection-safe. Otherwise, the returned set will
 -be nil and an error, described below, will explain the problem.
 +If successful, set will now be injection-safe. Otherwise, err is an error
 +defined in the docs for ErrorCode.
  
 -The template names do not need to include helper templates but should include
 -all names x used thus:\n-\n-    set.Execute(out, x, ...)\n-\n-EscapeSet modifies the named templates in place to treat data values as plain
-text safe for embedding in an HTML document. The escaping is contextual, so
-actions can appear within JavaScript, CSS, and URI contexts without introducing'hazards.
+HTML templates treat data values as plain text which should be encoded so they
+can be safely embedded in an HTML document. The escaping is contextual, so
+actions can appear within JavaScript, CSS, and URI contexts.
  
  The security model used by this package assumes that template authors are
  trusted, while Execute's data parameter is not. More details are provided below.
  
  Example
  
--    tmpls, err := new(template.Set).Parse(`{{define "t'"}}Hello, {{.}}!{{end}}`)\n-\n-when used by itself\n-\n-    tmpls.Execute(out, "t", "<script>alert('you have been pwned')</script>")
-+    import "template"\n+    import "template"\n     ...\n+    t, err := (&template.Set{}).Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)\n+    err = t.Execute(out, "T", "<script>alert('you have been pwned')</script>")
  
  produces
  
      Hello, <script>alert('you have been pwned')</script>!\n  
 -but after securing with EscapeSet like this,\n+but with contextual autoescaping,\n  
--    tmpls, err := EscapeSet(tmpls, "t")\n-    tmpls.Execute(out, "t", ...)\n+    import "html/template"\n+    ...\n+    t, err := (&template.Set{}).Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)\n+    err = t.Execute(out, "T", "<script>alert('you have been pwned')</script>")
  
 -produces the safe, escaped HTML output
 +produces safe, escaped HTML output
  
      Hello, &lt;script&gt;alert('you have been pwned')&lt;/script&gt;!\n  
  
  
  Contexts
  
--EscapeSet understands HTML, CSS, JavaScript, and URIs. It adds sanitizing
++This package understands HTML, CSS, JavaScript, and URIs. It adds sanitizing
  functions to each simple action pipeline, so given the excerpt
  
    <a href="/search?q={{.}}"&gt;{{.}}</a>
  
--EscapeSet will rewrite each {{.}} to add escaping functions where necessary,
++At parse time each {{.}} is overwritten to add escaping functions as necessary,
  in this case,\n  
    <a href="/search?q={{. | urlquery}}"&gt;{{. | html}}</a>
@@ -134,8 +129,8 @@ embedding in JavaScript contexts.\n  
  Typed Strings
  
 -By default, EscapeSet assumes all pipelines produce a plain text string. It
 -adds escaping pipeline stages necessary to correctly and safely embed that
-+By default, this package assumes that all pipelines produce a plain text string.\n+It adds escaping pipeline stages necessary to correctly and safely embed that
  plain text string in the appropriate context.\n  
  When a data value is not plain text, you can make sure it is not over-escaped
  @@ -183,8 +178,8 @@ injecting the template output into a page and all code specified by the
  template author should run as a result of the same."\n  
  Least Surprise Property
 --"A developer (or code reviewer) familiar with HTML, CSS, and JavaScript;
--who knows that EscapeSet is applied should be able to look at a {{.}}\n++who knows that contextual autoescaping happens should be able to look at a {{.}}\n  and correctly infer what sanitization happens."\n  */\n  package html

コアとなるコードの解説

html/template/template.go の役割

この新しいファイルは、html/template パッケージのユーザー向けAPIを再定義しています。以前は text/templateSetTemplate を直接使用し、その後 html/template.Escapehtml/template.EscapeSet を呼び出す必要がありました。しかし、この変更により、html/template パッケージ自体が text/template の機能を内部に持ち、エスケープ処理を自動的に行うようになりました。

  • type Set struct { ... template.Set }: html.Settext/template.Set を匿名フィールドとして埋め込んでいます。これにより、html.Settext/template.Set のすべてのメソッド(Parse, Add など)を自動的に継承します。
  • type Template struct { ... *template.Template }: 同様に、html.Templatetext/template.Template へのポインタを埋め込んでいます。
  • func (s *Set) Execute(...) error: このメソッドが、自動エスケープのトリガーとなります。テンプレートが実行される直前に、escapeSet 関数(以前の EscapeSet の非公開版)が呼び出され、テンプレートのASTがエスケープ処理のために書き換えられます。一度エスケープされたテンプレートは s.escaped[name] = true でマークされ、以降の実行では再エスケープされません。
  • func New(name string) *Template: text/template.New と同様に、新しいテンプレートインスタンスを作成しますが、返されるのは html.Template 型です。
  • func Must(t *Template, err error) *Template: text/template.Must と同様のヘルパー関数で、エラーが発生した場合にパニックを引き起こします。

これらの変更により、開発者は html/templatetext/template とほぼ同じように使用できるようになり、エスケープ処理の呼び出しを意識する必要がなくなりました。

escape.go の変更

EscapeEscapeSet が非公開関数 (escape, escapeSet) に変更されたことで、これらの関数は html/template パッケージの内部実装の詳細となり、外部から直接アクセスできなくなりました。これは、APIのクリーンアップと、エスケープ処理が自動的に行われるという新しい設計思想を反映しています。

doc.go の変更

ドキュメントの更新は、この変更の意図を明確に伝える上で非常に重要です。新しいドキュメントでは、html/templatetext/template をラップし、コンテキストに応じた自動エスケープが透過的に行われることが強調されています。これにより、開発者は html/template を使用するだけで、XSS攻撃から保護された安全なHTML出力を生成できるというメリットを理解しやすくなりました。

関連リンク

参考にした情報源リンク

  • Web search results for "golang html/template contextual autoescaping history golang.org/cl/5270042": (具体的なURLは提供されていませんが、html/template のコンテキストに応じた自動エスケープに関する一般的な情報源を参照しました。)
    • Go's html/template package provides robust contextual autoescaping, a critical security feature designed to prevent code injection vulnerabilities, particularly Cross-Site Scripting (XSS). This mechanism automatically escapes data based on the specific context in which it is being inserted, such as within HTML, JavaScript, CSS, or URI attributes.
    • The core principle behind html/template's autoescaping is its ability to understand the structure of HTML and apply the appropriate escaping rules. For instance, data inserted into a <script> tag will be JavaScript-escaped, while data in a standard HTML element will be HTML-escaped. This context-awareness ensures that user-supplied or dynamic content is safely rendered, preventing malicious code from being executed in the user's browser.
    • The package is a drop-in replacement for text/template and should be used whenever generating HTML output to leverage its security benefits. While the specific changelog golang.org/cl/5270042 was not directly found in public search results, the history of html/template shows continuous development and refinement of its autoescaping capabilities, with discussions and improvements documented in various Go project issues and commits over time.