モダンPerlの世界へようこそ

第42回Template Toolkit:Perl製テンプレートエンジンのデファクトスタンダード

組み合わせ自由なツールキット

Template Toolkit通称TTは、その名前からもわかるように、もともとは単なるテンプレートエンジンではなく、テンプレートエンジンをつくるためのツール群をまとめたものです。そのツール群を組み合わせた標準のエンジン、標準のフロントエンドと呼べるものもありますが、これはあくまでもTTのよくある利用法のひとつであって、そのすべてではありません。

たとえば、CPANにはApache::Templateという、TTのエンジン部分をmod_perl用にカスタマイズしたうえでmod_perl用のフロントエンドをかぶせるモジュールがありますが、これを使えば、最初に多少の設定は必要になるものの、あとはTTのテンプレートを適切なパスに置くだけで、パラメータの取得からルーティング、レンダリングまでよしなに計らってくれるようになります(TTの文法をサポートしたPHPのようなものになります⁠⁠。また、CPANにはあがっていませんが、O'Reilly社から出ている分厚い解説本(いわゆるアナグマ本)には、Mail::Templateという、TTのテンプレートと埋め込む値を渡したらメールを作成・送信してくれるフロントエンドも紹介されています。

もちろん差し替えられるのはフロントエンドだけではありません。たとえば、テンプレートに埋め込む変数やオブジェクトの処理は、もっとも基本的な構成ではTemplate::Stashというモジュールが行いますが、TTのディストリビューションにはその処理を高速化するTemplate::Stash::XSが同梱されていますし、ウェブ系の人であれば外部テンプレートのエンコーディングの問題を解決するためにCPANにあるTemplate::Provider::Encodingをインストールして使っている方も多いでしょう。そのほかにも、CPANには、かつて標準添付されていたデータベースやXMLを扱うものもあわせて大小170以上のプラグインが登録されていますし、その気になればテンプレートの文法規則自体をがらりと差し替えてしまうこともできます。

テンプレート話の3回目となる今回は、そのようなTTの拡張まわり、とりわけ最近のウェブ業界におけるセキュリティ対策の一環として覚えておきたいいくつかのモジュールについてまとめてみます。

Template::Provider::Encoding

上でも例にあげたTemplate::Provider::Encodingは、直接セキュリティの向上に貢献するものではありませんが、文字化け対策になるのはもちろん、文字数をカウントしたり正規表現を使ったフィルタを適用するときも問題が起こりづらくなるので、日本語を扱うテンプレートでは積極的に使っておきたいモジュールのひとつといえます。

use strict;
use warnings;
use Template;
use Template::Provider;

my $tt = Template->new({
    LOAD_TEMPLATES => [ Template::Provider::Encoding->new ],
});

ただし、Template::Provider::Encodingに同梱されているTemplate::Stash::ForceUTF8については、連載第32回でも紹介したように問題を複雑にしてしまううえ、パフォーマンス面でも問題となりうることがわかってきたので、最近では使わないようにするのがベタープラクティスとなっています[1]⁠。

このエンコーディング問題についてはTT本体のほうでも対応が進められています。最初にUnicode対応が行われたのは2004年10月リリースのバージョン2.14で、このときはテンプレートにBOMがついている場合のみPerlの内部表現に変換していましたが、2006年5月にリリースされた2.15では(同年1月にリリースされたTemplate::Provider::Encodingの影響もあって)ENCODINGオプションを渡しておけば、BOMがついていない外部テンプレートも指定したエンコーディングでデコードされるようになりました。この機能については更新履歴などへの記載がなく、ドキュメントでもほとんど触れられていないのですが、扱うテンプレートのエンコーディングがひとつに限られている場合は追加のモジュールをインストールしなくてすむ分気軽に使えます(ただし、複数エンコーディングには対応していないので、携帯対応などでエンコーディングの異なるテンプレートを使い分ける必要がある場合はテンプレートごとにエンコーディングを指定できるTemplate::Provider::Encodingを使ってください⁠⁠。

use strict;
use warnings;
use Template;

my $tt = Template->new({ ENCODING => 'utf8' });

このように内部表現に変換したテンプレートは、そのまま標準出力に送ろうとすると(日本語などが含まれている場合)ワイド文字の警告が出ます。一般的なウェブアプリケーションフレームワークを使っている場合はふつうフレームワーク側で対応済みなので気にする必要はありませんが、スクリプトの中などでテンプレートをそのまま出力したい場合は、いったん変数に受けてからEncode::encodeするなり、binmodeを使って出力用のファイルハンドルに変換用のレイヤをかますなりする必要があります。

binmode STDOUT => ':utf8';
$tt->process('tmpl.tt', \%vars) or die $tt->error;

# または

$tt->process('tmpl.tt', \%vars, \my $output) or die $tt->error;
print Encode::encode(utf8 => $output);

Template:Stash::AutoEscape

TTはもともと汎用のツールキットであるだけに、デフォルトでは特にウェブ用のエスケープなどは行いません。そのため、XSSなどの問題を防ぐには変数を埋め込む箇所に明示的にhtmlなどのフィルタをあてる必要がありますが、このような仕組みではテンプレートの記述量が増えるうえに、フィルタのあて忘れやあて間違いも発生しやすく、しばしば批判の対象となってきました。

この問題に対する対策はいくつか存在しますが、比較的単純な要件の場合はma.la氏によるTemplate::Stash::AutoEscapeを使っておくのが簡単です。

use strict;
use warnings;
use Template;
use Template::Stash::AutoEscape;

my $tt = Template->new({
    STASH => Template::Stash::AutoEscape->new,
});

$tt->process(\(my $tmpl =<<'TMPL'), {tag => '<foo>'}) or die $tt->error;
<div>[% tag %]</div>
<div>[% tag.raw %]</div>
TMPL

Template::Stash::AutoEscapeを使うと、特に指定がない変数の埋め込みはすべてHTML用のフィルタを経由するようになります。また、何らかの理由で生のHTMLを埋め込みたい場合はrawという仮想メソッドを利用することでエスケープを回避できます。

このrawメソッドはファイルのインクルードやHTMLを含むマクロなどを書くときに重宝しますが、利用頻度が高いマクロなどでは埋め込むときに毎回rawを指定するのも面倒です。このような場合は、初期化の際にオプションを指定しておけば、特定のマクロやフィルタのエスケープを常時回避できるようになります(以下の例では、hl_rawマクロにはrawメソッドを書かなくてもよいようにしています⁠⁠。

use strict;
use warnings;
use Template;
use Template::Stash::AutoEscape;

my $tt = Template->new({
    STASH => Template::Stash::AutoEscape->new({
        ignore_escape => [qw/hl_raw/],
    }),
});

$tt->process(\(my $tmpl =<<'TMPL'), {tag => '<foo>'}) or die $tt->error;
[% MACRO hl(s) GET '<span class="highlight">' _ s _ '</span>' -%]
[% MACRO hl_raw(s) GET '<span class="highlight">' _ s.raw _ '</span>' -%]
[% USE hlformat = format('<span class="highlight">%s</span>') -%]
<div>[% hl('searched word').raw %]</div>
<div>[% hl('<foo>searched word</foo>').raw %]</div>
<div>[% hl_raw('<foo>searched word</foo>') %]</div>
<div>[% hlformat('<foo>searched word</foo>').raw %]</div>
TMPL

Template::Plugin::JavaScriptとTemplate::Plugin::JSON::Escape

Template::Stash::AutoEscapeは便利なモジュールですし、単純なHTMLの場合はこれだけでも十分なセキュリティ対策になりますが、JavaScriptに渡すパラメータを埋め込む場合はさらにJavaScript用の対策を追加する必要があります。問題の詳細についてはma.la氏の記事が非常に行き届いているのでそちらをご覧いただくとして、ここではこの用途で使えるモジュールとして、Template::Plugin::JavaScriptと、もうひとつ、先日公開されたばかりのTemplate::Plugin::JSON::Escapeというモジュールの名前をあげておきます。前者はJavaScript全般、後者はJSONに特化したプラグインで、この原稿を書いている時点ではJSONに固有の\u2028、\u2029問題に対応しているかどうかが両者の顕著な違いといえます。

use strict;
use warnings;
use Template;
use Template::Plugin::JavaScript;
use Template::Plugin::JSON::Escape;
use JSON;

my $tt = Template->new;

my $vars = {
    value => '<foo>',
    hash => {foo => '<bar>'},
    jsonstr => encode_json({foo => '<baz>'}),
};

$tt->process(\(my $tmpl =<<'TMPL'), $vars) or die $tt->error;
[% USE JavaScript -%]
[% USE JSON.Escape -%]
<script>
var value = '[% value | html | js %]';
var hash = [% hash.json %];
var hash = [% jsonstr | json %];
</script>
TMPL

なお、同様のモジュールにMooseベースで書かれたTemplate::Plugin::JSONがありますが、こちらはいまのところセキュリティ系のエスケープはしてくれないため、必要に応じて追加のフィルタをあててください。

Template::Directive::XSSAudit

Template::Directive::XSSAuditは、TTのエンジンの奥深く、テンプレートを解釈してPerlコードに変換する部分に手を入れて、値を埋め込むときに適切なフィルタ(デフォルトではhtmlフィルタかuriフィルタ)があたっていない場合は警告が出るようにしてくれるものです。

use strict;
use warnings;
use Template;
use Template::Directive::XSSAudit;

my $tt = Template->new({
    FACTORY => 'Template::Directive::XSSAudit',
});

$tt->process(\(my $tmpl =<<'TMPL'), {tag => '<foo>'}) or die $tt->error;
[% USE JavaScript -%]
<div>[% tag %]</div>
<div>[% tag | html %]</div>
<script>var value='[% tag | js %]'</script>
TMPL

これを実行すると、以下のようにフィルタがまったくあたっていない箇所や、登録されていないフィルタしかあたっていない箇所がわかります。

<unknown_file>  NO_FILTERS      line:0  tag
<unknown_file>  NO_SAFE_FILTER  line:0  tag     js
<div><foo></div>
<div>&lt;foo&gt;</div>
<script>var value='\x3cfoo\x3e'</script>

もう少し細かいチェックが必要な場合は、on_filteredやon_errorにコールバック関数を渡すことで自前のチェックを追加することもできます。以下の例では、変数名に「_js」という文字列が含まれている場合、htmlフィルタやuriフィルタだけでなくjsフィルタもあたっていないと警告を出すようにしています。

use strict;
use warnings;
use Template;
use Template::Directive::XSSAudit;

my $tt = Template->new({
    FACTORY => 'Template::Directive::XSSAudit',
});

Template::Directive::XSSAudit->on_filtered(sub {
    my $e = shift;
    if ($e->{variable_name} =~ /_js/) {
        unless (grep { $_ eq 'js' }, @{ $e->{filtered_by} || [] }) {
            my $file = $e->{file_name} || '<unknown file>';
            my $line = $e->{file_line} || 0;
            warn "$file\tREQUIRES JS FILTER\tline:$line\t$e->{variable_name}\n";
        }
    }
});

$tt->process(\(my $tmpl =<<'TMPL'), {value_js => q/'+alert(1)+'/}) or die $tt->error;
[% USE JavaScript -%]
<script>
var value = '[% value_js | html %]';
var value = '[% value_js | html | js %]';
</script>
TMPL

Template::AutoFilter

Template::AutoFilterは、最初に紹介したApache::Templateなどと同じく、フロントエンド(と、付随するエンジンの一部)自体を差し替えて自動エスケープに対応しようというものです。使い方は素のTTとほとんど同じですが、こちらはTemplate::Stash::AutoEscapeと違って埋め込む値自体には特に手を加えず、単にフィルタがかかっていないところに指定のフィルタ(デフォルトではhtmlフィルタ)を追加するだけなので、たとえば複数のフィルタをあてたい部分には明示的に複数のフィルタをあてておく必要がありますし、フィルタをあてたくない部分には明示的にnoneという専用のフィルタをあてる必要があります。


use strict;
use warnings;
use Template::AutoFilter;

my $tt = Template::AutoFilter->new;

$tt->process(\(my $tmpl =<<'TMPL'), {tag => '<foo>'}) or die $tt->error;
[% USE JavaScript -%]
<div>[% tag %]</div>
<div>[% tag | none %]</div>
<script>var value='[% tag | html | js %]'</script>
TMPL

TT3

TTが非常に柔軟な(場合によっては生のPerlを書けるテンプレートエンジンよりも融通が利く)テンプレートエンジンであるのは誰もが認めることですし、国内ではSledge海外ではMaypoleや、その後を継いだCatalystといったウェブアプリケーションフレームワークが標準のテンプレートとして採用してきたこともあって、TTはいまでもPerl製テンプレートエンジンのデファクトスタンダードとみなされていますが、ベースとなるモジュールの開発が始まったのは1996年のことですから、いまとなってはさまざまな点で古めかしくなっていますし、柔軟すぎる代償として、実行速度などでも不利を抱えています。

そのため、2009年末にロンドンで開催されたワークショップでは作者のアンディ・ウォードリー(Andy Wardley)氏自身が「Template Toolkit 2はもう捨てたい」と述べていましたし、github上では、次期バージョンの最初のプロトタイプとなるTemplate-TT3に続き、2010年7月からはさらなる高速化をねらって、hempという、将来的にはTT3の心臓部となるであろうCライブラリの開発も始まっています。

ただ、その結果として、CPAN上ではもうかれこれ2年近くTT2の更新が止まっていますし、これまでの経緯を見る限り、TT3が実用段階にたどり着くのはまだまだ先のことでしょうから、速度が必要な現場や省メモリが必要な現場ではTTの代用品を求める動きも出てきています。次回はその動きについてまとめていきます。

おすすめ記事

記事・ニュース一覧