なぜPHPアプリにセキュリティホールが多いのか?

第36回MOPS:コンテクストを検出するHTMLエスケープ

第32回 PHPセキュリティ月間(Month of PHP Sercurity)「PHPセキュリティ月間」(MOPS - Month of PHP Security)について簡単に紹介しました。

今回もMOPS関連の話題です。MOPSではPHP関連のセキュリティ製品やセキュリティ知識の論文を募集し、11の論文が公開されました。今回はコンテクストを検出してエスケープするテンプレートエンジンについて紹介します。

MOPS Submission 02 – Context-aware HTML escaping
http://www.php-security.org/2010/05/05/mops-submission-02-context-aware-html-escaping/index.html

このテンプレートエンジンはNette Latteと呼ばれています。このテンプレートエンジンを独立して使用することもできますが、Nette Frameworkの一部として開発されています。 ホームページを見るとNette Frameworkの利用者としてチェコ首相のサイトも挙っています。チェコではかなり評判になっているフレームワークだと思われます(Nette Latteはチェコ語であると思われるので正しい発音が分かりません。ご存知の方は筆者にご連絡頂ければ幸いです⁠⁠。

コピーライトを見ると2004年からとなっており、最近作られたフレームワークではないので既にご存知の方も多いと思います。チェコ語と英語でホームページが作られているので恐らくチェコの方が作っているフレームワークだと思われます。筆者はこのテンプレートエンジンの存在をMOPSで知ったのですが、まだ今回のこの記事を執筆するまで使ったことがありませんでした。この記事はMOPSに投稿された論文とサイトの文書から執筆しています。

コンテクストを検出するHTMLテンプレートエンジンとは?

JavaScriptインジェクションを防ぐようHTMLを記述することは簡単ではありません。HTMLの中に記述される情報はテキストやHTMLでマークアップされたテキストだけではありません。タグの属性、URL、CSS、JavaScript、JavaScriptのテキストなど、それぞれに合ったエスケープ方法でエスケープしないとJavaScriptインジェクションの原因になります。

変数を記載する場所を自動的に検出してエスケープするテンプレートエンジンは「コンテクストを検出するHTMLテンプレートエンジン(Context aware HTMLtemplate engine⁠⁠」と呼ばれています。有名な例ではGoogleがオープンソースとして公開しているC++用のctemplateがあります。今回、紹介するNette LatteはPHPでコンテクストを検出できるテンプレートエンジンです。

Nette Latteが検出するコンテクスト

Nette Latteは以下のコンテクストを検出して適切なエスケープ処理を行います。

  • HTMLテキスト
  • "または'で囲まれたHTML属性
  • <script>または<style>で囲まれたCDATAのセクション
  • HTML属性のstyleおよびonclickなどのイベンドハンドラ
  • HTMLコメント

URLを属性値とする属性(href, src)はエスケープすべき部分(クエリ文字列パラメータ名と値)とそれ以外の部分が混在するためサポートされていないようです。

Nette Latteの利用例

どのように利用するのかは利用例を見るとすぐ分かります。

<script type="text/javascript">
var userId = {$userId};
</script>;
<p style="color: {$color};" title="{$title}">;
<a href="" onclick="return !confirm({$message});">{$desc}</a>;
</p>
<!-- Executed in: {$time} s -->;

Nette Latteのテンプレートでは各変数をそのまま出力しているように記述しますが、変数はそれぞれのコンテクストに合わせて適切な方法でエスケープされます。&ltscript&gtタグの中を見るとJavaScriptの変数がクオートで囲まれていないことに気付くと思います。Nette Latteは出力されるPHP変数のデータ型に合わせてJavaScript変数を出力する便利な機能を持っています。数値型はJavaScriptの数値として、文字列型はJavaScriptの文字列として、連想配列はJavaScriptのオブジェクトリテラルとして出力されます。

Nette Latteには自動的なエスケープを停止するオプションはありません。その代わりに {!$var} と変数の前に ! を追加します。⁠Nette Latteが検出するコンテクスト」でも記載しましたが、Nette LatteはURL属性を検出しません。URLリンクは代わりに {link} マクロを使う仕様になっています。

Nette Latteのマクロ

ほかのテンプレートエンジンと同様にさまざまなマクロをサポートしています。

標準のマクロ
{$variable}
テンプレート変数の出力
{!$variable}
テンプレート変数をエスケープなしで出力
{*text comment*}
テンプレート用のコメント
{plink ...}
presenterのリンク
{link ...}
urlの生成
{if ?} ... {elseif ?} ... {/if}
<?php if (?): ?> ... <?php elseif (?): ?> ... <?php endif ?>
{foreach ?} ... {/foreach}
<?php foreach (?): ?> ... <?php endforeach ?>
{for ?} ... {/for}
<?php for (?): ?> ... <?php endfor ?>
{while ?} ... {/while}
<?php while (?): ?> ... <?php endwhile ?>
{include dir/file.phtml}
テンプレートのインクルード
{var foo => value}
テンプレート変数宣言
{default foo => value}
デフォルト値の設定
{control loginForm}
ログインフォーム
{dump $variable}
変数のダンプ

これだけでも十分な機能を持っていますが、追加のマクロも利用できるようになっています。

追加のマクロ
{=expression}
<?php echo htmlSpecialChars(expression) ?>
{!=expression}
<?php echo expression ?>
{?expression}
PHPコードを評価
{_expression}
トランスレーション
{!_expression}
エスケープ無しのトランスレーション
{ifCurrent}
{if}がアクティブリンクの場合の特別なif
{include 'dir/file.phtml'}
テンプレートの読み込み
{cache ?} ... {/cache}
キャッシュチェック
{snippet ?} ... {/snippet}
制御スニペット
{attr ?}
HTMLタグ属性の登録
{capture $var} ... {/capture}
出力バッファリングを行い$varに保存
{block | texy} ... {/block}
texy ブロック
{widget ...}
コンポーネントをレンダリング
{control ...}
widgetのエイリアス
{contentType ?}
HTTPヘッダのContent-Typeを送信
{debugbreak}
ブレークポイントの挿入

Nette Latteの試用

Nete Latteが含まれているNette Frameworkは3つバージョンが用意されています。

  • PHP 5.2用でクラス名にプレフィックス有り
  • PHP 5.2用でクラス名にプレフィックス無し
  • PHP 5.3用で名前空間を使用

執筆時点での最新版は1.0のα版でした。PHP 5.3用をダウンロードして試用しました。ほんの少ししか使いませんでしたが、使ってみた感想は割とよくできているような印象でした。Webアプリケーションを作る場合もデフォルトどおりならほとんどコードを書く必要がありません。今時のフレームワークらしくデバッグを有効にしていると、エラーをGUIでわかりやすく表示します。xdebugにも対応しており、xdebugがロードされていると利用するようになっています。今時のMVCフレームワークとしての機能は一通り揃っています。ただし、CakePHPのようなコードを自動生成する機能は無いようです。

Nette Latte テンプレートを使ってみる

MOPSの文書ではNette Latteテンプレートは独立して利用できると記述されていましたが、このバージョンでは独立して利用する仕様が変更されたのか、使ってみるとNette FrameworkのベースオブジェクトであるObjectが定義されていない、とエラーになりました。このため、Nette Latte単独で使用するのではなくフレームワークが提供しているローダーを使用しました。ローダーを利用するとNette Frameworkを利用するために必要なファイルがロードされます。

テスト用コード: template.php(PHP 5.3用)
  1 <?php
  2 require_once __DIR__ . '/../../Nette/loader.php';
  3 
  4 use Nette\Forms\Form, Nette\Debug, Nette\Templates, Nette\Templates\Filters;
  5 
  6 Debug::enable();
  7 
  8 // absolute filesystem path to the web root
  9 const WWW_DIR=__DIR__;
 10 
 11 // absolute filesystem path to the application root
 12 const APP_DIR=__DIR__;
 13 
 14 // absolute filesystem path to the libraries
 15 define('LIBS_DIR', __DIR__ . '/../../3rdParty');
 16 
 17 // テンプレートオブジェクトの作成
 18 $template = new Nette\Templates\Template;
 19 // Nette Latteフィルタの登録
 20 $template->registerFilter(new Nette\Templates\LatteFilter);
 21 $template->setFile( 'template.phtml' );
 22 // テンプレート変数の登録
 23 $template->userId = 1234;
 24 $template->arrayVar = array('a'=>1, 'b'=>1, 'c'=>3);
 25 $template->stringVar = 'JS string <>"\'%';
 26 $template->color = 'expression(); red"';
 27 $template->title = 'ABC';
 28 $template->url = 'http://www.es-i.jp/';
 29 $template->message = 'Are you sure? "<>%';
 30 $template->desc = '<Click Here>';
 31 $template->time = '1234 -->';
 32 // テンプレートを出力
 33 echo $template;
 34 

テンプレートファイルは先程のサンプルテンプレートに多少付け加えたものを用意しました。

テンプレートファイル: template.phtml
  1 <html>
  2 <head>
  3 </head>
  4 <body>
  5 <script type="text/javascript">
  6 var userId = {$userId};
  7 var arrayVar = {$arrayVar};
  8 var stringVar = {$stringVar};
  9 </script>;
 10 <p style="color: {$color};" title="{$title}">;
 11 <a href="{$url}" onclick="return !confirm({$message});">{$desc}</a>;
 12 </p>
 13 <!-- Executed in: {$time} s -->;
 14 </body>

Nette Latteのマクロとは{macro名}のように{}で囲まれた部分がマクロとしてテンプレートエンジンで置換されます。

{link}マクロと使おうとしたところエラーが発生したため、ソースコード見てみるとNetteフレームワークのコントローラオブジェクトを利用するようになっていました。{link}マクロの使い方が

{link コントローラ名:アクション名, パラメータ}

となっているので当然と言えば当然です。フレームワークを使っていない場合、URL属性等のエスケープ処理はすべてプログラマが行わなければなりません。URLのベース部分とクエリ部分を連想配列で渡し、適切にエスケープするマクロがあるとより安全になると思います。

コマンドラインからtemplate.phpを実行した結果
<html>
<head>
</head>
<body>
<script type="text/javascript">
var userId = 1234;
var arrayVar = {"a":1,"b":1,"c":3};
var stringVar = "JS string <>\"'%";
</script>;
<p style="color: expression\(\)\;\ red\&quot;;" title="ABC">;
<a href="http://www.es-i.jp/" onclick="return !confirm(&quot;Are you sure? \&quot;&lt;&gt;%&quot;);">&lt;Click Here&gt;</a>;
</p>
<!-- Executed in: 1234 --><!--> s -->;
</body>

いくつかエスケープすべき文字を入れてみましたが、Nette Latteは正しくエスケープ処理をしているようです。今回は色々試してエスケープ処理の堅牢さをチェックしませんでしたが、基本的なエスケープ処理は正しく動作していました。

まとめ

Nette Latteはデフォルトでコンテクストに合わせたスケープ処理を行い、エスケープ処理を無効にするオプションはありません。Nette Latteのようなテンプレートエンジンを利用するとJavaScriptインジェクションを減らすことができると考えられます。コンテクストの検出にオーバーヘッドが必要ですが、一度テンプレートがパースされるとキャッシュされます。コンテクスト検出のオーバーヘッドはあまり大きくないと考えられます。

ほかのフレームワークと組み合わせて使用する場合、Nette Frameworkのloader.phpを利用すると多くの不必要なファイルをインクルードしてしまうことが気になると思います。各フレームワークでNette Latteのようなコンテクスト検出型のテンプレートエンジンが利用できるようになると、より安全なPHPアプリケーションが増えるでしょう。ほかのフレームワーク開発者の努力に期待したいところです。

おすすめ記事

記事・ニュース一覧