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