Closure Templatesのオートエスケープが最強すぎる件

この記事を読んで、ちょうど最近使っているGoogle Closure Templatesがいい感じだったので紹介します。

コンテキストが異なる/重なるポイントでのエスケープ問題

最近のほとんどのテンプレートエンジンでは、変数埋め込みをデフォルトでHTMLエスケープしてくれます。が、元記事で指摘されているように、それでは正しくないケースがあります。HTML PCDATA以外のコンテキストで文字列を生成したり、複数のコンテキストが重なっている箇所です。


極端な例としてはこんな感じです。

<a href="{$x1}" onclick="alert('{$x2}')">{$x3}</a>
<script>
  var x = '{$x4}';
  var y = {$x5};
</script>
<style>
  p {
    font-family: "{$x6}";
    background: url(/images?q={$x7});
    left: {$x8};
  }
</style>

{$x1} みたいなところに文字列が出力されるとして、それぞれどんなエスケープをするのが適切でしょうか?


x1はHTMLエスケープですが、href属性に予期せず"javascript:**"とかきたら大変なのでフィルタリングも必要そうです。
x2はコンテキストが重なっている箇所で、JavaScript文字列としてエスケープしたあとにHTMLとしてエスケープします。


x4はJavaScript文字列としてエスケープが必要です。
x5はNumber型の変数にしたいのでシングルクォートで囲っていませんが、文字列が数値以外だったら危ないのでチェックが必要ですね。


x6はCSS文字列としてのエスケープ。
x7はURLコンポーネントとしてエンコードしますが、普通のURLエンコードではカッコ"(", ")"が抜けてきちゃうので要注意。
x8にはクォートがないので、expressionとか来たら大変です。そっちのフィルタリングも必要そうです。


といった感じで、(この例は極端ですが)これらを全てミスなくケアしていくのは正直言って厳しいと思います*1

Contextual Autoscape

そこでGoogle Closure Templatesはどうしてるかというと、テンプレートをプリコンパイルでパースして、各出力場所のコンテキストを判別しちゃいます。


上の例をClosure Templatesで書くとこんな感じで、

{namespace template autoescape="contextual"}

/**
 * @param x Maybe dangerous string!
 */
{template .render}
<a href="{$x}" onclick="alert('{$x}')">{$x}</a>
<script>
  var x = '{$x}';
  var y = {$x};
</script>
<style>
  p {lb}
    font-family: "{$x}";
    background: url(/images?q={$x});
    left: {$x};
  {rb}
</style>
{/template}

これをJavaScriptにプリコンパイルするとこうなります*2

template.render = function(opt_data) {
  return '<a href="' + soy.$$escapeHtmlAttribute(soy.$$filterNormalizeUri(opt_data.x)) + '" onclick="alert(\'' + soy.$$escapeJsString(opt_data.x) + '\')">' + soy.$$escapeHtml(opt_data.x) + '</a><script>var x = \'' + soy.$$escapeJsString(opt_data.x) + '\'; var y = ' + soy.$$escapeJsValue(opt_data.x) + ';<\/script><style>p {font-family: "' + soy.$$escapeCssString(opt_data.x) + '"; background: url(/images?q=' + soy.$$escapeUri(opt_data.x) + '); left: ' + soy.$$filterCssValue(opt_data.x) + ';}</style>';
};

読みにくいですが、コンテキストごとに異なるエスケープ関数が割り当てられているのが分かると思います*3。("soy" というのはClosure Templatesのコードネームです。)

で、これに実際に、

{ x: 'javascript:/*</style></script>/**/ /<script>1/(alert(1337))//</script>' }

という「いかにも」な文字列を食わせるとこうなります。

<a href="zSoyz" onclick="alert('javascript:\/*\x3c\/style\x3e\x3c\/script\x3e\/**\/ \/\x3cscript\x3e1\/(alert(1337))\/\/\x3c\/script\x3e')">javascript:/*&lt;/style&gt;&lt;/script&gt;/**/ /&lt;script&gt;1/(alert(1337))//&lt;/script&gt;</a>
<script>
  var x = 'javascript:\/*\x3c\/style\x3e\x3c\/script\x3e\/**\/ \/\x3cscript\x3e1\/(alert(1337))\/\/\x3c\/script\x3e';
  var y = 'javascript:\/*\x3c\/style\x3e\x3c\/script\x3e\/**\/ \/\x3cscript\x3e1\/(alert(1337))\/\/\x3c\/script\x3e';
</script>
<style>
  p {
    font-family: "javascript\3a \2f \2a \3c \2f style\3e \3c \2f script\3e \2f \2a \2a \2f \2f \3c script\3e 1\2f \28 alert\28 1337\29 \29 \2f \2f \3c \2f script\3e ";
    background: url(/images?q=javascript%3A%2F*%3C%2Fstyle%3E%3C%2Fscript%3E%2F**%2F%20%2F%3Cscript%3E1%2F%28alert%281337%29%29%2F%2F%3C%2Fscript%3E);
    left: zSoyz;
  }
</style>

こちらも分かりにくいですが、"<"のエスケープのされ方でコンテキストが認識されていることが分かると思います。HTMLでは"&lt;"、JSでは"\x3c"、CSSでは"\3c "、URLでは"%3C"になってます。
"zSoyz"というのは、フィルタリングでひっかかったところです。href属性に"javascript:"を指定したりしてる部分ですね。


ここで上げた例は公式ドキュメントに載っているもので、他にもいくつかの例が載ってるので興味がある人は見てください。(ここで使ったサンプルの出力結果がドキュメントの方で間違ってるみたいなので注意。)

まとめ

ということで、Closure Templatesでみんな幸せになれます!
オフィシャルにはJavaJavaScriptの実装しかないけど、一応言語中立って言ってて他の言語のバインディングも作れるようになってるので、Ruby版やPHP版を作ったら喜ばれるんじゃないでしょうか*4

とはいえ

テンプレートというvalidでないHTMLをパースするわけで、このコンテキスト認識はもちろん完璧じゃないです。

というか、コンパイラに理解不能なテンプレートってことはきっと人間にも理解困難なので、そもそもそんな難しいテンプレート書いちゃダメってことなんだと思います。

あれ、じゃあこんなに高度なオートエスケープなんていらないかw

*1:さらには、ここでは「JavaScript文字列としてエスケープ」などと適当に流していますが、それぞれのコンテキストでの適切なエスケープ方法やフィルタリング方法だってそれほど自明なことではありません。その辺りをもう少し詳しく知りたい人は id:amachang の[https://docs.google.com/present/view?id=dq8psxc_379gpn4v2hq:title=資料(P29あたりから)]がオススメ。

*2:言い忘れてましたが、Closure TemplatesはJavaJavaScriptから使えるテンプレートエンジンです。

*3:実際の各関数の定義を知りたい方は[http://code.google.com/p/closure-templates/source/browse/trunk/javascript/soyutils.js:title=このあたりのソースを読んでください]。

*4:[https://github.com/archimag/cl-closure-template:title=Common Lisp版]は作ってる人がいるみたいですよ。