素の PHP をテンプレートとして使うフレームワークは今でも結構あるようです。
素の PHP をテンプレートに使う、というのがどういうことかというと、簡単な例ですが次のようなものです。
index.php
<?php $name = filter_input(INPUT_GET, 'name'); include __DIR__ . '/index.html.php';
index.html.php
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> Hello <?= $name ?> </body> </html>
php -S localhost:8888
でビルドインウェブサーバを起動して http://localhost:8888/?name=oreore
にアクセスすると Hello oreore
と表示されることでしょう。
しかし、↑のコードは $name
がエスケープされておらずよろしくありません。
http://localhost:8888/?name=%3Cscript%3Ealert%28%27!!!%27%29%3C/script%3E
などとアクセスすると !!! と alert されてしまいます。
これを防止するためには、テンプレートで変数を表示するときに htmlspecialchars をかます必要があります。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> Hello <?= htmlspecialchars($name, ENT_QUOTES) ?> </body> </html>
さすがに毎度 <?= htmlspecialchars($name, ENT_QUOTES) ?>
などと書くのはめんどくさいので、フレームワークによっては別の書き方が提供されています。
例えば・・・
- Zend Framework は
<?= $this->escapeHtml($name) ?>
とすればエスケープされます- あまり楽になっている気がしないです
- CakePHP は
<?= h($name) ?>
と書けば良いらしいです- こういうグローバル関数がたくさん定義されているようです
- FuelPHP は変数をビューにアサインしたときに自動でエスケープされるようです
Security::htmlentities
を見た感じオブジェクトをアサインするときに困りそうです
Twig も Smarty も自動的にエスケープする機能を持っており、かつ、FuelPHP のようなオブジェクトをアサインしたときの問題もありません。素の PHP でも同じようなことがやりたいです。。。
.
.
.
やってみました。
index.php
<?php require __DIR__ . '/../vendor/autoload.php'; use PhpRenderer\Renderer; $data = [ 'name' => "<script>alert('!')</script>", 'html' => "<strong>safe string</strong>", ]; echo (new Renderer)->render(__DIR__ . '/index.html.php', $data);
index.html.php
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> Hello <?= $name ?>. <br> None Escape <?php echo $html ?> </body> </html>
<?= $name ?>
のようなショートタグを使うと自動的にエスケープされます<?php echo $html ?>
のように普通の PHP タグを使うとエスケープされません
↑の例は次のような HTML になります。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> Hello <script>alert('!')</script>. <br> None Escape <strong>safe string</strong></body> </html>
実装方法
PHP にはストリームラッパーという便利なものがあります。fopen などのファイルシステム関数に http://
とかを渡すことができるあれです。
実は require や include にもストリームラッパーが使えます(いくつかのストリームラッパーは allow_url_include
を on にしなければ使えません)。
例えば compress.zlib ストリームラッパーで gzip 圧縮された PHP のコードを include してみます。
<?php echo "this is gzip\n";
index.php
<?php include 'compress.zlib://' . __DIR__ . '/gzip.php.gz';
$ gzip gzip.php $ php index.php this is gzip
そして、ストリームラッパーは独自のものを新たに登録することもできます。
なので、独自のストリームラッパーを作れば require や include するときに PHP のコードをプリプロセスすることができます。
実装コード
実装したコードは下記にあります。
StreamWrapper.php
がストリームラッパーの実装クラスです。php.renderer://
のようなスキームで呼べるように登録します。
rewrite()
で PHP のコードを書き換えています。 正規表現でざっくり はさすがに怖い気がしたので token_get_all()
の字句解析の結果を元に T_OPEN_TAG_WITH_ECHO
と T_CLOSE_TAG
を書き換えています(<?=
と ?>
です)。
問題点
opcache
素の PHP ですが opcache は効きません。opcache は file:// と phar:// 以外のストリームラッパーはキャッシュしないようです。
なので、コードの書き換えは都度発生します。
ストリームラッパーを使わずに、書き換えたコードをどこかに保存してそのまま include すれば大丈夫ですが・・・当初の目論見ではストリームラッパーによって変換された後の opcode がキャッシュされるかなーと思ってたので残念です。
parse error
単純に
<?= $name ?>
を
<?=StreamWrapper::html( $name )?>
と書き換えているだけなので、
<?= $name; ?>
などと書かれると
<?=StreamWrapper::html( $name; )?>
となってパースエラーになります。
その他
普段まったく使わないのですっかり忘れていましたが asp_tags
なんてものもありました。
asp_tags
を有効にして <%=
の形式のときだけ自動エスケープを行うのも良いかもしれません。
さいごに
社内で勉強会したときのスライド。若干ネタ成分あり。