2010年11月アーカイブ

HTML内のJavaScriptに値を安全に埋め込む際のフィルタモジュー JavaScript::Value::Escape がVersion 0.04になってます。ちょっと時間が経っているけど紹介

まず、0.04でgfx氏によりドキュメントが増えたり修正されました。感謝感謝です。あと、javascriptvalueescape っていう関数名が長いので、jsという名前でもExportすることができるようになっています

use JavaScript::Value::Escape qw/js/;

です。テンプレートエンジンに組み込む際にはこっちの名前にしたほうがわかりやすいとは思いますが、直接jsをテンプレートエンジンに設定するより、escape_html と組み合わせて関数を自前で用意するのが個人的にお勧め。

もう一つが動作の変更で、Version 0.01 でよりもEscape対象となる文字が増えています。Version 0.04 では、q!”!、q!’!、q!&!、q!=!、q!-!、q!+!、q!;!、q!!,、q!/!、q!! 、と\x{2028}(LINE SEPARATOR)、\x{2029}(PARAGRAPH SEPARATOR)、さらにアスキーの制御文字、\x00-\x1f、\x7fがEscapeの対象となりました。\x{2028}、\x{2029}を扱うので、Perlのテキスト文字列を利用するのが推奨です。ただしバイナリ文字列でもエラーにはなりません。

エスケープする文字にはおそらくエスケープする必要のない文字が含まれていますが、これはDjangoのテンプレートエンジンにあわせた物となっています。

JavaScript::Value::EscapeをPerlテキスト文字列で利用すればおそらく安全にHTML内のJavaScriptに値を埋め込むことができると思いますが、それ以降の処理でXSSを起こさないようJavaScriptのコードとテキストの内容に気を使う必要があるのは変わっていませんのでご注意ください。

CloudForecastに何人かの方から要望があった複数サーバの一括表示機能がやっと付きました。

cloudforecast-servers.png

作ってみたら便利で、ライブドアでは既に活用しまくってます。チューニング後の比較や一部のサーバに問題があった場合の確認などにも使えると思います。もちろん右上の日付範囲指定もできるので、特定の期間にしぼってグラフを表示することも可能です。

cloudforecast-top.png

複数のサーバを表示する時は、サーバリストページで、サーバ名の左側のチェックボックスにチェックを入れます。Gmailと同じくShiftキー+マウスクリックで範囲選択することができます。そして上部メニューの「View Selected Hosts」ボタンをクリックします。選択したサーバはこのボタン横のチェックボックスのチェック外すことで全て解除できます。

さらに、サーバリソースデータの取得状態を確認する機能も追加しました。チェックボックスの左側の「✖」の色でステータスを表現します。全てのデータが取得できている場合、薄いグレーとなり、一つでも取得できない場合はオレンジ色になります。またCPUやメモリ使用量のグラフが設定されていて、SNMP経由でそれらの値がとれない場合には「✖」が赤色に変化します。

CloudForecastでは通知の機能がないため、Nagiosのような稼働監視はできませんが、SNMPが正しく設定されているかなどリソース監視の動作確認、また撤去したサーバが監視に残ったままになっていないかなどの確認に利用することができます。

この変更のため、CloudForecast の依存モジュールに「JSON」が追加されてました。git pullして最新バージョンにする際には気をつけてください。

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

Log::Minimal-0.03でオブジェクトやリファレンスをDumpするメソッドを追加しました。

warnf( "dump is %s", ddf({ foo => bar }) );

とddfメソッドが追加されました。上は

local $Data::Dumper::Terse = 1;
local $Data::Dumper::Indent = 0;
warnf( "dump is %s", Data::Dumper::Dumper({ foo => bar }) );

と同じです。

短く書けていいよね!

ddfとか追加せずに自動でDumpしてもいいかなと思ったんだけど、URIオブジェクトとかだと結果が

bless( do{\(my $o = 'http://example.com/')}, 'URI::http' )

こうなる。Data::Dumperでは自動でstringfyしない。これはなんとなく欲しい情報ではない気がして個別にddfをかます方を選択しました。URIだけを取りたいのであれば、

my $uri = URI->new("http://example.com/");
warnf("uri is %s", $uri);

でいけるので、もっと簡単

Data::Rmapとoverload::Methodでどうにかとか思ったけど依存増えるので辞めました。

あと、sprintfのフォーマットを増やすのも考えたんだけどtokuhiromに指摘された通り、将来perlのsprintfが拡張された際、互換性に問題がでそうなのでやめておきました。

もっと簡単でいい方法があれば、採用したいと思いますのでネタプリーズ

追記
CPANリリースしました
http://search.cpan.org/dist/JavaScript-Value-Escape/
/追記

malaさんの「HTMLのscriptタグ内に出力されるJavaScriptのエスケープ処理に起因するXSSがとても多い件について」にちょろっとでているgistのコードをモジュールにしました。

JavaScript::Value::Escape - https://github.com/kazeburo/JavaScript-Value-Escape

JavaScript::Value::EscapはHTMLのscriptタグ内にデータを埋め込む際に、少々過剰にエスケープを行うものです。このモジュールではq!”!, q!’!, q!&!, q!>!, q!<!, q!/!, q!\!, qq!\r! と qq!\n! を\u00xxなどに変換します。変換する理由等は上記のエントリによく書かれています。

単独で使う事はあまりなく、テンプレートエンジンと組み合わせて使うことがほとんどだと思います。

use JavaScript::Value::Escape;
use Text::Xslate;

my $tx = Text::Xslate->new(
    syntax => 'TTerse',
    function => {
        js => sub {
            javascript_value_escape(@_);
        },
    }
);

$tx->render_string(<<'EOF',
<html>
<body>
<script>
document.write('[% test | html | js %]');
</script>
<a onclick="alert('[% test | js %]')">alert</a>
</body>
</html>

javascript_value_escapeは、&もエスケープするため、jsフィルタ後にhtmlフィルタを通っても全く問題ありません。rawフィルタを使うなどの例外がなくなります。ただし、malaさんが書いている通り、適用順序を間違えるとXSSが起こりやすいのと、jsフィルタだけで直接document.writeやinnerHTMLに入れ込むとXSSが起こる可能性があります。これを防ぐには、jsフィルタを

javascript_value_escape(Text::Xslate::Util::escape_html(@_));

にしてしまうことが考えられます。あとは、jsの中ではjsフィルタを使う事を確認することになりそうですが、これは機械的にチェックできるかな。

問題なさそうであれば、CPANにあげようと思います。

Apache、mod_perlにおけるpnotesのようにリクエスト単位でデータを保持できるPlack::Middlewareをリリースしました。実装にはScope::Containerを利用しています。

使い方は簡単Plack::BuilderでScope::Containerのmiddlewareを有効にします

use Plack::Builder;

builder {
    enable "Scope::Container";
    $app;
};

とくにオプションなどはありません。

これでリクエストがあった際にScope::Containerのstartscopecontaierを呼んでくれるので、アプリケーションのなかでscope_containerを使って簡単にデータの出し入れが行えます。

以下はHTTP接続をリクエストの間だけKeepAliveするサンプル

package MyApp::UA;

use Scope::Container;
use LWP::UserAgent;

sub getua {
    if ( in_scope_container() && my $ua = scope_container('ua') ) {
        return $ua;
    } else {
        my $ua = LWP::UserAgent->new(keep_alive=>1);
        scope_container('ua', $ua) if in_scope_container();
        return $ua;
    }
}

package MyApp;

sub app {
  my $env = shift;
  $ua = MyApp::UA->getua(); # new instance
  $ua = MyApp::UA->getua(); # from container cache
  $ua = MyApp::UA->getua(); # from container cache
  return [ '200', [] ["OK"]];
  # disconnect keepalive and destroy $ua
}

もちろんWebアプリケーションだけに限れば、$envでも実現できることは多いですが、Scope::Containerを使うことでモデル層のモジュールをWebのリクエスト/レスポンスの世界とを疎となるように構築し、必要な場所に限ってMiddlewareでそれを結びつけることができます。

Log::Minimal v0.02をリリースしました。xaicronさんのpatchにより出力するログのレベルを変更することができるようになりました。

WARN以上のログだけを出したい場合、$Log::Minimal::LOG_LEVEL を変更します。

use Log::Minimal;
local $Log::Minimal::LOG_LEVEL = "WARN";

infof("info"); #出力されない
warnf("warn"); #出力される

デフォルトのレベルはDEBUGです。なお、デバックメッセージを出すには環境変数も設定されている必要があります。

ところで、Log::Minimalでは4つのログレベル、DEBUG、INFO、WARN、CRITICALをサポートしていますが、他のLogモジュールではより詳細なレベルを用意しているものがあります。そのため、アプリケーションの開発中にどのレベルでログを出力すればいいのか悩んでしまうことがあるかもしれません。その結果ログを出す事に躊躇してしまうことがあったりなかったり。

そのような際には、ログをアプリケーション開発者と運用エンジニアのコミュニケーション手段として考えて、見る人にやって欲しい事をベースに考えてみるのはいかがでしょうか。Log::Minimalでサポートしている4つのレベルを例にすると以下のようになります。

LEVEL 見る人 内容
DEBUG 開発者 開発中のデバック出力。プロダクション環境では出力されない
INFO 開発者 開発者自身が運用中に参照する情報。アプリケーションの問題を発見するための手がかりとして
WARN 運用者/開発者 運用中に起きたシステムに関する問題(入力チェックに引っかかったとかではない)。ただし直ぐにサービスが継続できない状態ではないもの。障害を検知するための間接的な情報
CRITICAL 運用者/開発者 サービスの継続ができないシステムに関する障害。障害原因を探すための直接的な情報


運用するのは自分だよという開発者の方は、夜間の障害時に欲しい情報だったり、夜間のパフォーマンスに関する問題を翌営業日に調査するのに十分な情報を出すイメージを持つと良いかもしれません。

ログを出す基準が決まると、自信をもってログが出せるようになり、障害時の対応も速くなり、アプリケーションの改善が進み、夜間対応が減り、みんなでリア充になれます(たぶん)

運用におけるエラーログの重要性もしくはLog::Minimalってモジュール書いた話」で書いたLog::Minimalを、Plackと組み合わせたときにさらに便利になるMiddlewareを書きました。

ここで入手可能になるはず
http://search.cpan.org/dist/Plack-Middleware-Log-Minimal/

試しにログを吐くだけのアプリケーションを作ってリクエストをすると

pmlogminimal.png

こんな風に、ログに色をつけてくれるのと、REQUEST_URIも付加してくれます。ただし色が付くのは開発環境(plackup -E productionとかしない場合)です。

Log::MinimalとPlack::Middleware::Log::Minimalの最大の特徴として粗結合+グルーとなるミドルウェアという組み合わせが挙げられます。Log::Minimal自体は特に依存がないので、Web以外のcronなどでも普通に動きます。Plack::Middlewareを使った場合はMiddlewareがログを書き出す部分をフックし、付加情報を自動で付け、最適な方法で書き出します。具体的には$envのREQUEST_URIをログに追加し、psgi.errorsに対してログを書き出すように変更しています。さらにcronやjobqueue用のフレームワークがLog::Minimalをサポートすればそれぞれの環境にあった方法でログを出力することができると思われます。

サンプルに使ったpsgiファイル

use Plack::Builder;
use Log::Minimal;

builder {
  enable 'Log::Minimal';
  sub {
     debugf("debug");
     infof("info");
     warnf("warn");
     critf("crit");
     [200,['Content-Type'=>'text/plain'],['OK']];
  }
};

簡単で便利なので気軽にログを出すようにするといいと思います。

追記
CPANリリースしました
http://search.cpan.org/dist/Scope-Container/
/追記

mod_perl のアプリケーションでは、Apacheモジュールの提供するpnotesを使うとリクエスト毎のデータを簡単に持つことができます。pnotesに入れたデータはリクエストの処理が終了したところで自動的にクリーンアップされます。これを利用したのがリクエストごとにインスタンスを作成破棄できる、Apache::Singleton(::Request)です。

また、pnotesはデータベースの接続の管理にもしばしば使われます。1リクエストを裁いている間だけデータベースとの接続を維持し、リクエストが完了したところで接続を閉じるような処理に利用されています。このようにすることでmod_perlのプロセス数分(数百)の接続がMySQLに常に張られることもなく、また1度のリクエストの中で何度も接続を行うコストを押さえる事ができます。

ソースコードにすると以下のような感じ

my $dbh;
if ( $ENV{'MOD_PERL'} ) {
    $dbh = Apache2::RequestRec->r->pnotes('dbh');
}
if ( !$dbh ) {
    $dbh = DBI->connect($dsn,$user,$pass,\%attr);
    if ( $ENV{'MOD_PERL'}  ) {
          Apache2::RequestRec->r->pnotes('dbh',$dbh);
    }
}
return $dbh;

pnotesに入れることで、リファレンスカウントが上がるので、アプリケーションで$dbhが使われなくなっても維持され、リクエストが終わったところで初めてオブジェクトが破棄されます。

時は変わって2010年代。PSGI/PlackでWebアプリケーションを作るのが当たり前になっていますが、その際にpnotesのような機能をどのように実現したらいいのか議論になることがあります。AmonやKamui、Pickelsと言ったフレームワークではリクエスト毎に生成されるContextを持っているので、これを利用するのが良さそうです。

しかし、複雑化していく現在のWebアプリケーションではWebからのアクセスをWebアプリケーションサーバが裁くだけではなく、GearmanやQ4M、ZeroMQといったJobQueue、メッセージングシステムもバックエンドで多く使われています。このバックエンド処理でも、1回のJobの処理中にWebアプリケーションのリクエストと同じように扱えるContextがあると便利です。データベースの接続はこのContextに保管することでJob処理中1回だけ行い、処理が終われば自動的に接続を廃棄できるはずです。

このようなContext機構を、WebアプリケーションではフレームワークのContext、Job Workerでは別のContextとならずに、1つの方法で実現できないか、さらにこの考え方を押し進めて、リクエストやJobだけに限らない任意のタイミング(スコープ)でContextの生成・破棄を行えないかということで書いてみたのが、Scope::Container です

githubにあげてあります: http://github.com/kazeburo/Scope-Container

(Scope::Contextなのかなと思う事あり)

実はこのような思想の元、書かれたモジュールがCPANには一つあります。mixiの広木さんの書いたScope::Sessionです。しかし、使い方がぱっとみでよくわかりませんでしたのでぜひ、解説記事求むです。

追記
解説記事書いて頂きました
Sessionの生成・破棄を任意のタイミングで制御可能にするScope::Session
/追記

Scope::Container を use すると2つのメソッドをexportします。基本的な使い方は以下のような感じ。

use Scope::Container;

sub foo {
    my $key = shift;
    scope_container($key);
}

{
    my $guard = start_scope_container();

    #データ保存
    scope_container('key',value);

    #データ取得
    my $value = scope_container('key');

    my $value2 = foo('key') #上と同じ結果

    #$guardがスコープから外れるので自動的に、保存したものは削除される
}

start_scope_container は Guardオブジェクトを返します。そしてこのオブジェクトが破棄されるまでを1つのスコープとして扱います。この間にデータの出し入れを自由に行え、$guardが破棄されるタイミングで全てのデータがクリアされます。

追記
gfxのpatchで最新版ではGuardに依存しなくなりました。
Scope::Containerオブジェクトを返します
/追記

そして以下がデータベースへの接続を管理するサンプル

sub connect {
    my $class = shift;
    my @dsn = @_;
    my $dbh;

    my $dsn_key = Data::MessagePack->pack(\@dsn);     
    my $connector = scope_container($dsn_key);
    eval {
        $dbh = $connector->_dbh;
    };
    return $dbh if $dbh;

    my $connector = DBIx::Connector->new(@dsn);
    $dbh = $connector->dbh;

    scope_container($dsn_key, $connector);
    return wantarray ? ( $dbh, $connector ) : $dbh;
}

dsn、ユーザ名、パスワード、オプションをMessagePackでシリアライズしたものをキーとしてScope::ContainerにDBIx::Connectorのオブジェクトを保持させます。複数スレーブ対応版のサンプルも書いてみたのでgistにあげてあります。

Scope::ContainerをPlackで使う場合は、Middlewareで

sub call {
    my ($self, $env) = @_;

    my $guard = start_scope_container();

    try {
        $self->app->($env);
    } catch {
        die $_;
    } finally {
        undef $guard;
    };
}

としておけばアプリケーションの中でscope_contaierが利用でき、GearmanのWorkerなら

my $guard;
$worker->work(
    on_start => sub {
        $guard = start_scope_container();
    },
    on_complete => sub {
        undef $guard;
    },
    stop_if => sub {
        ...
    }
);

としておけばWebアプリケーションと同じように動いてくれると思われます。

Scope::Containerはlocal $FOOBARで代用ができるところも多いと思いますが、汎用で使えるものとしてどう思いますでしょうか?