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

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

説明

ユーザー入力を適切にエスケープせずにHTMLへ出力すると、<script> タグや onclick などのイベントハンドラーを挿入されるおそれがあります。ブラウザーがその入力をページのコードとして解釈すると、被害者のブラウザーでJavaScriptが実行され、セッションの悪用、フィッシング、キー入力の記録などにつながります。

想定される影響

  • スクリプトから読めるCookieやトークンを盗まれたり、ユーザーの権限で要求を送られたりする可能性があります。
  • 悪意のあるJavaScriptがDOMの変更、ブラウザーの転送、キー入力の記録などを行うおそれがあります。
  • 偽のログイン・決済フォームによって、機密情報を入力させられる可能性があります。
  • 不正なポップアップや転送が信頼を損ない、金銭的・法的な損失につながる場合があります。

対処方法

  • HTML応答では html/template と信頼済みのテンプレートを使い、ユーザー入力を通常のデータとして渡してください。
  • http.ResponseWriter.Write() 自体が危険なのではありません。ユーザー入力を直接連結したHTMLを提供しないでください。
  • HTML、URL、JavaScriptなどの出力文脈に応じて処理し、ユーザー入力からテンプレート自体を作らないでください。
  • 外部入力を template.HTML、template.URL、template.JS など、信頼を表す型へ変換しないでください。
  • URLや名前の形式を検証しても、出力文脈に応じた処理を省略しないでください。

例

HTMLとして提供する応答を比較します。html/template のURL文脈処理は危険なスキームや属性の挿入を制限しますが、外部リンクのドメイン許可方針までは定めません。

変更前

go
package main

import (
    "fmt"
    "net/http"
)

// 変更前: ユーザー入力をResponseWriterへ直接書き込む
func errorPage(w http.ResponseWriter, r *http.Request) {
    // ユーザーが操作できるクエリパラメーター
    url := r.URL.Query().Get("url")

    // ユーザー入力を含むHTML
    const template = `
    <html>
    <body>
      <h1>error; page not found. <a href="%s">go back</a></h1>
    </body>
    </html>`

    // 文字列を組み立てて直接書き込む
    // urlには "javascript:alert(1)" や "\" onclick=\"alert(1)" などが入力され得る
    w.WriteHeader(http.StatusBadRequest)
    w.Write([]byte(fmt.Sprintf(template, url)))
}

func main() {
    http.HandleFunc("/error", errorPage)
    http.ListenAndServe(":8080", nil)
}

変更後

go
package main

import (
    "html/template"
    "log"
    "net/http"
)

// 変更後: html/templateによる自動エスケープ

// テンプレートの{{.URL}}位置へデータを渡し、
// html/templateが文脈に応じて自動エスケープする
var errorTmpl = template.Must(template.New("error").Parse(`
<html>
<body>
  <h1>error; page not found. <a href="{{.URL}}">go back</a></h1>
</body>
</html>`))

func errorPageSafe(w http.ResponseWriter, r *http.Request) {
    url := r.URL.Query().Get("url")

    // URLが空なら既定値を設定
    if url == "" {
        url = "/"
    }

    w.WriteHeader(http.StatusBadRequest)

    // データ構造をテンプレートに渡す
    data := struct {
        URL string
    }{URL: url}

    if err := errorTmpl.Execute(w, data); err != nil {
        log.Println("template execute error:", err)
    }
}

func main() {
    http.HandleFunc("/error", errorPageSafe)
    http.ListenAndServe(":8080", nil)
}

解説:

  • 変更前: fmt.Sprintf() でHTMLへ url パラメーターを直接挿入し、w.Write() でブラウザーへ送ります。" onclick="alert(1) などを入力されると<a> 属性の境界を抜け、JavaScriptを挿入されるおそれがあります。
  • 変更後: 信頼済みテンプレートの href 位置へ通常の文字列を渡します。html/template がURL文脈を処理し、危険なスキームや属性の境界からの脱出を制限します。ユーザー入力を template.URL や template.HTML へ変換して処理を迂回しないでください。

参考資料