弁護士ドットコム株式会社 Creators’ blog

弁護士ドットコムがエンジニア・デザイナーのサービス開発事例やデザイン活動を発信する公式ブログです。

Go HTML Template のエスケープの挙動に気をつけよう

アイキャッチ画像

TL; DR

  • Go HTML Template では、渡した文字列がデフォルトでエスケープされますが、Typed Strings を渡すとエスケープされません
  • そこにユーザーが自由に指定できる値を設定すると、XSS 脆弱性につながる恐れがあります
  • Revel の関数の中には、引数に渡した値を、内容はそのまま Typed Strings にして返すものがあります
  • すべての条件が揃うケースは稀ですが、気をつけましょう

Go の HTML テンプレート

html/template は Go の標準ライブラリです。
他の言語にも存在するような、HTML へのテンプレート展開を実現してくれます。

以下のコードが

<!-- greeting := "hello!" -->
<p> {{ .greeting }} </p>

このように変換されます。

<p> hello! </p>

便利な一方で、変数を HTML(の一部)として認識させるような箇所は、XSS 脆弱性と隣合わせです。
その対策として、デフォルトでは、HTML における特殊文字は暗黙的にエスケープされます。

以下のコードが

<!-- greeting := "<strong>hello!</strong>" -->
<p> {{ .greeting }} </p>

このように変換されます。

<p> &lt;strong&gt;hello!&lt;/strong&gt; </p>

しかし、この暗黙的なエスケープが動作しないケースがあります。

テンプレートに Typed Strings を渡すと文字列がエスケープされない

Typed Strings とは、 src/html/template/content.go で定義されている特別な文字列型で、 HTML 型や JS 型、CSS 型などが用意されています。
これらの型によって、その文字列が HTML や JavaScript であることを表すことができます。

Typed Strings がテンプレート内の適切な箇所に渡された場合、その文字列はエスケープされません1
自身で HTML などのコードを適用するときにエスケープをされては困るので「Typed Strings を用意した側が、その文字列の安全性を担保している」という前提に基づいて、これらの型を指定します。

以下のコードが

<!-- greeting := template.HTML("<strong>hello!</strong>") -->
<p> {{ .greeting }} </p>

このように変換されます。

<p> <strong>hello!</strong> </p>

Revel の関数の中には、引数に渡した値を、内容はそのまま Typed Strings にして返すものがある

ここからが厄介です。
クラウドサインでは Go の Web フレームワークである Revel を使用していますが、その関数の中には、引数に渡した値を、内容はそのまま Typed Strings にして返すものが存在します。
おまけにそのことを関数名から推測しづらいのです。

Revel のテンプレート向け関数群 の多くは、 HTML 型を返します。

ここでは、その中から msg 関数を見てみましょう。
これは、現在のロケールと渡したキーに対応した文字列を返してくれる、i18n 関数です。

細かな挙動は割愛しますが、この手の関数は、指定したキーが辞書に存在しない場合、そのキーをそのまま表示します。2
そのとき、この関数は、返り値を template.HTML でラップして返します。

つまり、 msg 関数に渡した値がキーとして存在しない場合、その渡した値が Typed String として返され、テンプレート上でエスケープされなくなります。

知っていればなんのことはないですが、関数の名前からはおよそ想像のつかない挙動です。

うっかりユーザー入力値を msg 関数に渡してしまったら、晴れて XSS 脆弱性の完成です。

<!-- $.someValue がユーザー入力値だったら…… -->
<p> {{ msg . $.someValue }} </p>

素朴で短い関数名なので、もしかしたらレビューでも見落としてしまうかも知れません。

まとめ

  • テンプレートに渡した文字列はデフォルトでエスケープされますが、Typed Stringsを渡すとエスケープされません
  • そこにユーザーが自由に指定できる値を設定すると、XSS脆弱性につながる恐れがあります
  • Revel の関数の中には、引数に渡した値を、内容はそのまま Typed Strings にして返すものがあります
  • すべての条件が揃うケースは稀ですが、気をつけましょう

  1. Go HTML Template は HTML, CSS, JavaScript あるいは URI を理解していて、適切な箇所に渡した場合のみエスケープが回避されます。たとえば、テンプレートの <script> 要素内に JavaScript のコードを渡す場合、エスケープを回避するには template.JS 型を使用しなければなりません。ほかの型(template.HTML 型など)を使用すると、エスケープされてしまいます。詳しくは、公式ドキュメントの ContextTyped Strings をご覧ください。
  2. msg 関数の場合、厳密には、引数に渡した値がそれ単体で返ってくるわけではなく値を含む形にフォーマットされて返されます。デフォルトでは、defaultUnknownFormat で定義されているように、 ??? 引数に渡した値 ??? の形式でフォーマットされて返されます。これにより、画面にレンダリングされた際に、存在しないキーであることがわかりやすくなります。