サンドボックス
サンドボックスは、テンプレートの中でどのタグ、PHP 関数、メソッドなどを使えるかを制御できるセキュリティ層です。サンドボックスモードのおかげで、アプリケーションを危険にさらしたり望まない操作が行われたりする心配なく、クライアントや外部のコーダーと安心してテンプレート作成を進められます。
どのように動くのでしょうか。テンプレートで許可したいものを定義するだけです。最初はすべてが禁止されていて、そこから少しずつ権限を与えていきます。次のコードは、テンプレートの作者に
{block}、{if}、{else}、{=} のタグ(最後のものは変数や式を出力するタグです)と、すべてのフィルタの使用を許可します。
$policy = new Latte\Sandbox\SecurityPolicy;
$policy->allowTags(['block', 'if', 'else', '=']);
$policy->allowFilters($policy::All);
$latte->setPolicy($policy);
個々のグローバル関数や、オブジェクトのメソッド・プロパティへのアクセスを許可することもできます。
$policy->allowFunctions(['trim', 'strlen']);
$policy->allowMethods(Nette\Security\User::class, ['isLoggedIn', 'isAllowed']);
$policy->allowProperties(Nette\Database\Row::class, $policy::All);
allowMethods() と allowProperties()
で与えた権限は、指定したクラスのサブクラスのインスタンスにも適用されます(チェックには
is_a() を使います)。
すごいと思いませんか。すべてを非常に低いレベルで制御できます。テンプレートが許可されていない関数を呼んだり、許可されていないメソッドやプロパティにアクセスしたりすると、Latte\SecurityViolationException
がスローされます。
安全なデフォルトポリシー
すべてが禁止された状態からポリシーを作るのは不便かもしれないので、安全な土台から始めることもできます。
$policy = Latte\Sandbox\SecurityPolicy::createSafePolicy();
この安全な土台では、contentType、debugbreak、dump、extends、import、include、layout、php、sandbox、snippet、snippetArea、templatePrint、varPrint、embed
を除くすべての標準タグが許可されます。標準フィルタは
datastream、noescape、nocheck を除いてすべて許可されます。さらに
$iterator オブジェクトのメソッドとプロパティへのアクセスも許可されます。
サンドボックスの有効化
ルールが適用されるのは、{sandbox}
タグで挿入したテンプレートです。これは {include}
とよく似ていて、サンドボックスモードを有効にし、{include}
と同様に周囲の変数を自動的には渡しません。ただし {sandbox 'untrusted.latte', a: 1, b: 2}
のように明示的に渡すことはできます。
{sandbox 'untrusted.latte'}
つまりレイアウトや個々のページはすべてのタグと変数を自由に使え、制限は
untrusted.latte テンプレートにだけ適用されます。
禁止されたタグやフィルタの使用のような違反はコンパイル時に検出されます。オブジェクトの許可されていないメソッドの呼び出しなど、それ以外は実行時に検出されます。テンプレートにはそれ以外のエラーが含まれていることもあります。サンドボックス内のテンプレートから出た例外がレンダリング全体を止めてしまわないよう、独自の例外ハンドラを定義できます。たとえばログに記録するだけ、といったこともできます。
すべてのテンプレートで直接サンドボックスモードを有効にしたい場合も簡単です。
$latte->setSandboxMode();
生成されたコードのチェック
構文としては正しいものの禁止されていて PHP
のコンパイルエラーを引き起こすコードを、ユーザーがページに挿入できないようにするため、テンプレートを PHP
リンターでチェックすることをおすすめします。この機能は Engine::enablePhpLinter()
メソッドで有効にできます。チェックのために PHP
バイナリを呼び出す必要があるので、そのパスをパラメータとして渡してください。
$latte = new Latte\Engine;
$latte->enablePhpLinter('/path/to/php');
サンドボックスが守らないもの
許可したタグ・関数・メソッド・プロパティとは別に、サンドボックスはポリシーに関係なくいくつかの構文を無条件に禁止します。new
演算子、$this 変数、可変変数($$var)、そして |noescape
フィルタです。
サンドボックスは明示的な操作、つまり関数・メソッド・フィルタの呼び出しやオブジェクトのプロパティへのアクセスを確実に守ります。ただし、そのチェックが届かないものがひとつだけあり、これは知っておく価値があります。
オブジェクトを出力したり、文字列が求められる場所で使ったりすると、PHP
はマジックメソッド __toString()
を自動的に呼びます。ポリシーはこの暗黙の変換をチェックしないので、__toString()
メソッドが許可されていなくても実行されます。つまりテンプレートの作者は、テンプレートから到達できる任意のオブジェクトで
__toString() を発動できます。これはサンドボックスがブロックする明示的な形
{$obj->__toString()} とは異なります。
{$obj} {* __toString() を呼ぶ *}
{$obj . '!'} {* 同じ(連結) *}
{="price: $obj"} {* 同じ(文字列補間) *}
{$obj|upper} {* 同じ(フィルタ経由) *}
__toString()
の目的はオブジェクトのテキスト表現を作ることなので、到達できること自体はたいてい害になりません。問題になるのは、__toString()
が副作用を持つ(データベースへの書き込みや問い合わせなど)場合か、機微なデータを返す場合だけです。
Latte
はこうした最も直接的なケースを捕まえることはできますが、すべてを確実にとらえられるわけではありません。オブジェクトを文字列に変換するのはメソッド呼び出しではなく言語に組み込まれた操作で、式の中の多くの場所で起こります。そのうちいくつか(たとえばオブジェクトと文字列の比較、あるいは呼び出された関数やフィルタの内部)では、正当な用途まで塞がずに確実に横取りすることができません。ですから
__toString() には到達できるものとして考えてください。
副作用を持つ、あるいは機微なデータを露出する __toString()
を備えたオブジェクトを、サンドボックスに渡さないでください。
これはテンプレートに直接渡すオブジェクトだけでなく、許可された関数・メソッド・プロパティの戻り値として作者が手に入れられるオブジェクトにも当てはまります。