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

このブログ記事について

このページは、Masahiro Naganoが2010年11月18日 19:26に書いたブログ記事です。

ひとつ前のブログ記事は「Log::Minimal で Data::Dumper をサポートしました」です。

次のブログ記事は「CloudForecastに複数サーバ一括表示とデータ取得状態の確認機能付きました」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

ウェブページ

OpenID対応しています OpenIDについて
Powered by Movable Type 4.27-ja