「HTMLのscriptタグ内にデータを埋め込む際のエスケープ処理モジュール書いた」に引き続いて、XSSを避けつつ複数の値をJSONで渡す方法。
答えはmalaさんが書いてます
- テンプレートエンジンでJSONを生成する(多くの場合間違えるので、推奨しない)
- scriptタグの中でJSONを使わない
- 可能であればJSONライブラリのオプションで<>/いずれかをエスケープする。
- 生成されたJSON文字列の<>/いずれかを正規表現などを使って置換する。
- JSONのvalueに当たる部分には「HTMLエスケープ済みの文字列を入れる」という規約を設けて事前にエスケープする。
の3番目以降。
ということで実装してみる。目標としてはXslateのfilterとして実装 [% hashref | json %] の様な形をとり、JSONのvalueにあたる部分はすべてHTML Escapeし、HTML中に安全に出力できるようフィルタする。
use JSON;
use Text::Xslate;
use Data::Rmap qw//;
use Scalar::Util qw(blessed);
my $tx = Text::Xslate->new(
syntax => 'TTerse',
function => {
json => sub {
my $hashref = shift;
Data::Rmap::rmap_to {
Data::Rmap::cut($_) if blessed $_;
return if ref $_;
$_ = Text::Xslate::unmark_raw(Text::Xslate::html_escape($_))
} Data::Rmap::ALL, $hashref;
my $json = JSON->new->ascii->encode($hashref);
my $bs = '\\';
$json =~ s!/!${bs}/!g;
$json =~ s!<!${bs}u003c!g;
$json =~ s!>!${bs}u003e!g;
$json =~ s!&!${bs}u0026!g;
Text::Xslate::mark_raw($json);
},
}
);
追記
encode_json を JSON->new->ascii->encode にしました。U+2028,U+2089への対応
/追記
これで、HTMLに埋め込んでも安全だと思われるJSONにするフィルタができあがります。Data::Rmapで再帰的にhtml_escapeして、encode_json。できあがったJSONから >/<& あたりをJavaScript Unicodeでescape。最後にmark_raw。mark_rawがないとsingle quotがXslateでhtml_escapeされるので必要。&はどちらでもいいはず。
使う時は、
<script>
var data = [% $hashref | json %];
document.write(data.foo); // HTML escapeされてる
</script>
となります
生の値が必要な場合は、HTMLをescapeする必要があります。JavaScriptでHTMLをescapeするには、どこからからコピペしてきたものですが、Stringのprototypeを拡張して
String.prototype.unescapeHTMLx = function () {
var temp = document.createElement("div");
temp.innerHTML = this;
var result = temp.childNodes[0].nodeValue;
temp.removeChild(temp.firstChild);
return result;
}
alert(data.foo.unescapeHTMLx());
といった方法が考えられます(このunescapeHTMLxはもっといい実装方法あるらしい)
もう一段階、JSONによるデータ受け渡しを使いやすくするために、JSONのmixiのMixi.Gatewayを真似てみてもいいと思います。mixiの実装では、headタグを閉じる前に、
<script type="text/javascript"><!--//<![CDATA[
if (!window['Mixi']) { window['Mixi'] = {}; }
if (!('Gateway' in window['Mixi'])) {
window.Mixi.Gateway = {
_params : {},
getParam:function(key){
var obj = Mixi.Gateway;
return obj._params[key];
},
setParam:function(value){
var obj = Mixi.Gateway;
obj._params=value;
}
};
}
Mixi.Gateway.setParam({ "foo":"bar" });
//]]>--></script>
このようなコードが入っており、テンプレートからJavaScriptへの値の引き渡しをMixi.Gatewayで一本化しているようです。これをほぼパクって参考にして、
JSGateway.setParam([% $hashref | json %]);
と、データをセット、
JSGateway.getParam('query');
JSGateway.getParam('query').unescapeHTMLx(); // 生データが欲しい場合
この様にするのがいいでしょうか。
フレームワークから利用する場合は、例えば、stashの適当な値をJSに渡す入れ物にして、コントローラ内からデータをセット。
# $c は Catalystライクなコンテキスト
$c->stash->{js_param} //= {};
$c->stash->{js_param} = $base_uri;
テンプレートのbaseでheadの閉じタグの前あたりに
Mixi.Gateway.setParam([% $c.stash.js_param | json %]);
をいれて置いたら、どのページでも使えるようになるので良いんじゃないでしょうか。全部くっつけたサンプルはgistに