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

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

説明

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型に変換しないでください。

参考資料