Phalcon\Filter でユーザー定義フィルタハンドラを使ってセキュリティ対策

Webアプリケーションではユーザーの入力値はバイナリを除けば全て文字列として取得するわけですが、その際にセキュリティ対策としてNULLバイトや制御文字などの不正な値を除去しないといけません。

アプリケーションフレームワークが独自のリクエストクラスを備えている場合、そういった便利機能も用意されていることはありますが、この手の処理はどんなアプリケーションでも共通して必要となるため、自分で書いたものを組み込んで利用できるのが一番だと思います。

そういった用途で使えるのが [Phalcon\Filter](http://docs.phalconphp.com/en/latest/api/Phalcon_Filter.html) です。

以下、コードの動作を確認した環境です。

* Windows 7
* PHP 5.6.7 ビルトインWebサーバ
* Phalcon 1.3.4


## Phalcon\Http\Request から Phalcon\Filter を使う

公式のAPIドキュメントには Phalcon\Filter について

> The Phalcon\Filter component provides a set of commonly needed data filters. It provides object oriented wrappers to the php filter extension.

とあり、以下のようなサンプルコードが掲載されています。

```php
<?php

$filter = new Phalcon\Filter();
$filter->sanitize("some(one)@exa\\mple.com", "email"); // returns "someone@example.com"
$filter->sanitize("hello<<", "string"); // returns "hello"
$filter->sanitize("!100a019", "int"); // returns "100019"
$filter->sanitize("!100a019.01a", "float"); // returns "100019.01"
```

これだけではそんなに便利な機能には見えないのですが、このオブジェクトにはユーザー定義のフィルタハンドラ（Phalcon\Filter と紛らわしいので以後「フィルタハンドラ」と呼びます）を登録する機能があり、登録したフィルタハンドラを [Phalcon\Http\Request](http://docs.phalconphp.com/en/latest/api/Phalcon_Http_Request.html) クラスのユーザー入力値を取得するメソッド（ get(), getPost(), getPut(), getQuery() ）から簡単に呼び出すことができます。

たとえば $_POST 変数から入力値を取得する getPost() メソッドの場合

> Gets a variable from the $_POST superglobal applying filters if needed

とあり、以下のようなサンプルコードが掲載されています。

```php
<?php

//Returns value from $_POST["user_email"] without sanitizing
$userEmail = $request->getPost("user_email");

//Returns value from $_POST["user_email"] with sanitizing
$userEmail = $request->getPost("user_email", "email");
```

このうち後者が "email" フィルタハンドラを通して取得する例です。すなわち ``$filter->sanitize("some(one)@exa\\mple.com", "email"); // returns "someone@example.com"`` と同じ処理が施されるわけです。

（Phalcon\Http\Request は [Phalcon\DI\InjectionAwareInterface](http://docs.phalconphp.com/en/latest/api/Phalcon_DI_InjectionAwareInterface.html) を実装していて、この機能を利用した場合だけDIを経由して Phalcon\Filter が利用されます。この辺は Phalcon のちょっと嫌なところなんですが…）


## Phalcon\Filter にユーザー定義のフィルタハンドラを登録して Phalcon\Http\Request から呼び出す

ここからが本題ですが、 Phalcon\Filter にユーザー定義のフィルタハンドラを登録して Phalcon\Http\Request から呼び出してみます。

以下はテキストを大文字に変換する "upper" フィルタハンドラを登録して、 Phalcon\Mvc\Micro で利用する例です。

public/index.php

```php
<?php
$loader = include realpath(__DIR__ . '/../vendor/autoload.php');

$di = new \Phalcon\DI();

$di->setShared('logger', function() {
    return  new \Phalcon\Logger\Adapter\File(
        __DIR__ . DIRECTORY_SEPARATOR . 'test.log'
    );
});

$di->setShared('router', function() use ($di) {
    $router = new \Phalcon\Mvc\Router();
    $router->setDI($di);
    return $router;
});

$di->setShared('request', function() use ($di) {
    $request = new \Phalcon\Http\Request();
    $request->setDI($di);
    return $request;
});

$di->setShared('response', function() use ($di) {
    $response = new \Phalcon\Http\Response();
    $response->setDI($di);
    return $response;
});

$di->setShared('filter', function() {
    $filter = new \Phalcon\Filter();
    $filter->add('upper', function($value) { // "upper" フィルタハンドラを登録する
        return strtoupper($value);
    });
    return $filter;
});

$app = new \Phalcon\Mvc\Micro($di);

$app->map('/', function () use ($app) {

    $text = $app->request->getPost('text', 'upper'); // "upper" フィルタハンドラを通して取得する

    if ($app->request->isPost()) {
        $app->logger->log(sprintf('POST / text=%s', $text));
    }

    return $app->response->setContent(sprintf(<<<HTML
<html>
<body>
<form method="post" action="/">
<p>
<input type="text" name="text" value="%s" />
<input type="submit" value="POST" />
</p>
</form>
</body>
</html>
HTML
    , htmlspecialchars($text)
    ));

})->via(['GET', 'POST']);

$app->handle();
```

テキストボックスとボタンだけの簡単なフォームですが、ここから "foo bar baz" と入力して送信した結果、test.log ファイルには以下のように出力されました。

```
[Tue, 31 Mar 15 12:13:55 +0900][DEBUG] POST / text=FOO BAR BAZ
```

``Phalcon\Filter::add()`` の第1引数がハンドラ名、第2引数がハンドラ関数となりますが、ハンドラ関数は [callable](http://php.net/manual/ja/language.types.callable.php) を全て受け付けてくれるので、無名関数以外にも配列 ``array($obj, 'myCallbackMethod')`` や 文字列 ``'MyClass::myCallbackMethod'`` あるいは [__invoke()](http://jp2.php.net/manual/ja/language.oop5.magic.php#object.invoke) 実装オブジェクトでもOKです。（こういうところでインタフェースの実装を要求されないのはいいですね）


## 不正な制御文字を除去して改行コードを統一するフィルタハンドラの例

冒頭のセキュリティ対策を実現するためのフィルタハンドラの例です。

再利用やテストのしやすさを考えて、__invoke() メソッドを持つクラスとして実装しています。

src/Acme/Phalcon/Filter/Normalize.php

```php
<?php
/**
 * Example application for Phalcon\Mvc\Micro
 *
 * @copyright k-holy <k.holy74@gmail.com>
 * @license The MIT License (MIT)
 */
namespace Acme\Phalcon\Filter;

/**
 * Normalizeフィルタ
 *
 * @author k.holy74@gmail.com
 */
class Normalize
{
    public function __invoke($value)
    {
        return $this->normalizeNewline(
            $this->removeControlCharacters($value)
        );
    }

    public function removeControlCharacters($value)
    {
        // HT,LF,CR,SP以外の制御コード(00-08,11,12,14-31,127,128-159)を除去
        return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]|\xC2[\x80-\x9F]/S', '', $value);
    }

    public function normalizeNewline($value)
    {
        // 改行コードを統一
        return str_replace("\r", "\n", str_replace("\r\n", "\n", $value));
    }
}
```

今回 Phalcon\Filter のサンプルとして用意したため名前空間を ``Acme\Phalcon\Filter\Normalize`` としていますが、内容は Phalcon フレームワークに依存するものではありません。


先ほどのサンプルコードを少し書き換えた、利用側のコードです。名前空間 ``Acme\\`` は Composerのオートロードで ``src/Acme`` をパスに設定しています。

public/index.php

```php
<?php
$loader = include realpath(__DIR__ . '/../vendor/autoload.php');

$di = new \Phalcon\DI();

$di->setShared('logger', function() {
    return  new \Phalcon\Logger\Adapter\File(
        __DIR__ . DIRECTORY_SEPARATOR . 'test.log'
    );
});

$di->setShared('router', function() use ($di) {
    $router = new \Phalcon\Mvc\Router();
    $router->setDI($di);
    return $router;
});

$di->setShared('request', function() use ($di) {
    $request = new \Phalcon\Http\Request();
    $request->setDI($di);
    return $request;
});

$di->setShared('response', function() use ($di) {
    $response = new \Phalcon\Http\Response();
    $response->setDI($di);
    return $response;
});

$di->setShared('filter', function() {
    $filter = new \Phalcon\Filter();
    $filter->add('normalize', new \Acme\Phalcon\Filter\Normalize()); // "normalize" フィルタハンドラを登録する
    return $filter;
});

$app = new \Phalcon\Mvc\Micro($di);

$app->map('/', function () use ($app) {

    $text = $app->request->getPost('text', 'normalize'); // "normalize" フィルタハンドラを通して取得する

    if ($app->request->isPost()) {
        $app->logger->log(sprintf('POST / text=%s', $text));
    }

    return $app->response->setContent(sprintf(<<<HTML
<html>
<body>
<form method="post" action="/">
<p>
<textarea name="text">%s</textarea>
<input type="submit" value="POST" />
</p>
</form>
</body>
</html>
HTML
    , htmlspecialchars($text)));
})->via(['GET', 'POST']);

$app->handle();
```

画面上では確認しづらいのですが、フォームのテキストエリアから入力した改行コードが test.log ファイルでは全て LF に統一されました。（Windows環境なのでログの改行自体は CR + LF になるようです。 Phalcon\Logger の仕様？）

```
[Tue, 31 Mar 15 10:12:22 +0900][DEBUG] POST / text:foo
bar
baz
```

不正な制御コードは例外処理としてエラーを返すべき、改行コードの統一は入力値の取得時ではなくサーバーへの保存時にやるべき、という考え方もありそうですが、面倒なので大体こんな風にしています。

``getPost('text', 'normalize')`` といちいち指定するだけでも面倒なので、普通に ``getPost()`` した時でも処理を通してくれるような設定があればベストだと思うのですが、そうすると ``Phalcon\Http\Request`` に手を入れるなりラッパーを用意せざるを得ないので大変です。

あとセキュリティ対策では [mb_check_encoding()](http://jp.php.net/manual/ja/function.mb-check-encoding.php) も必須ですが、こちらはどこでやるのが適切なんでしょうね。

Micro だったら micro:beforeExecuteRoute イベント、 Dispatcherだったら dispatch:beforeExecuteRoute イベント辺りに入れるのでしょうか？ ``Micro::handle()`` や ``Dispatcher::forward()`` のたびに呼ばれてしまうのが難点ですが…