説明
Goの text/template はHTMLを自動でエスケープしません。タイトル、コメント、プロフィールなどのユーザー入力や外部データをHTMLへ直接出力すると、<script>...</script> などのスクリプトが別のユーザーのブラウザーで実行されるおそれがあります。保存データやリクエストパラメーターを通じて、セッションの悪用、ページの改ざん、フィッシングなどに使われる可能性があります。
想定される影響
- スクリプトから読めるCookieやトークンを盗まれたり、ユーザーの権限で要求を送られたりするおそれがあります。
- JavaScriptがキー入力の記録、API呼び出し、ブラウザー内のデータへのアクセスを行う可能性があります。
- 偽のログイン・決済フォームを表示され、機密情報を入力させられるおそれがあります。
- DOMを変更され、表示内容の偽造や追加の要求などにつながる可能性があります。
対処方法
- HTMLの描画には
text/templateではなくhtml/templateを使ってください。一般のテキスト生成にtext/templateを使うこと自体は問題ではありません。 - テンプレート自体は信頼済みのコードに固定し、外部の値を通常の文字列として渡して文脈に応じたエスケープを適用してください。
- ユーザー入力を
template.HTML、template.JSなどの信頼された型に変換しないでください。 - 追加の防御として適切なCSPでスクリプト実行を制限してください。
- 入力形式を検証しつつ、不審な文字列の除外だけで出力エンコードを代替しないでください。
例
変更前
go
package main
import (
"html"
"log"
"net/http"
"text/template" // HTMLの自動エスケープなし
)
// 単純な投稿データ構造
type Post struct {
Title string
Content string // ユーザーが自由に入力する内容
}
// text/templateはHTMLをエスケープしない
var tmpl = template.Must(template.New("post").Parse(`
<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
<h1>{{.Title}}</h1>
<div>
내용: {{.Content}}
</div>
</body>
</html>
`))
func handler(w http.ResponseWriter, r *http.Request) {
// 例: クエリパラメーターからタイトルと内容を受け取る
title := r.URL.Query().Get("title")
content := r.URL.Query().Get("content")
p := Post{Title: title, Content: content}
if err := tmpl.Execute(w, p); err != nil {
log.Println(err)
http.Error(w, html.EscapeString("template error"), http.StatusInternalServerError)
return
}
}
func main() {
http.HandleFunc("/post", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
変更後
go
package main
import (
"html"
"html/template" // HTMLの文脈に応じた自動エスケープ
"log"
"net/http"
)
// 単純な投稿データ構造
type Post struct {
Title string
Content string // ユーザーが自由に入力する内容
}
// html/templateで自動エスケープ
var tmpl = template.Must(template.New("post").Parse(`
<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
<h1>{{.Title}}</h1>
<div>
내용: {{.Content}}
</div>
</body>
</html>
`))
func handler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Query().Get("title")
content := r.URL.Query().Get("content")
p := Post{Title: title, Content: content}
if err := tmpl.Execute(w, p); err != nil {
log.Println(err)
http.Error(w, html.EscapeString("template error"), http.StatusInternalServerError)
return
}
}
func main() {
http.HandleFunc("/post", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
解説:
- 変更前:
text/templateは{{.Content}}の値をHTML用にエスケープしません。content=<script>alert('xss')</script>などの入力が、生成されたページで実行されるスクリプトになるおそれがあります。 - 変更後:
html/templateは信頼済みテンプレートの文脈に合わせて通常の文字列を処理します。この例の{{.Content}}はテキスト位置なので、<script>などもタグではなくテキストとして表示されます。外部入力を信頼されたHTML型に変換しないでください。