Phalconのチュートリアル(チュートリアル 1)にCSRF対策を追加してみる

この記事はPhalcon Advent Calendar 2014の16日目です。昨日はyohgakiさんの「Phalcon 1.3 と 2.0のベンチマーク」でした。

Phalconのチュートリアル(チュートリアル 1)」には、CSRF対策がありませんでした。そこで追加してみようと思います。

PhalconでのCSRF対策についての公式ドキュメントは以下になります。

フォームにトークンを追加

まず、フォームにトークンを追加します。

--- a/app/views/signup/index.phtml
+++ b/app/views/signup/index.phtml
@@ -4,6 +4,10 @@

 <?php echo Tag::form("signup/register"); ?>

+<!-- CSRF対策のトークン -->
+<input type="hidden" name="<?php echo $this->security->getTokenKey() ?>"
+    value="<?php echo $this->security->getToken() ?>"/>
+
  <p>
     <label for="name">Name</label>
     <?php echo Tag::textField("name") ?>

POSTメソッドのリクエストでトークンをチェック

コントローラで$this->security->checkToken()メソッドを使い、トークンをチェックすればいいのですが、今回は、すべてのPOSTメソッドで自動的にトークンをチェックするようにしてみます。

--- a/public/index.php
+++ b/public/index.php
@@ -11,6 +11,47 @@ try {
     //Create a DI
     $di = new Phalcon\DI\FactoryDefault();

+    // CSRF対策にセッションを使うので自動でスタートするように
+    $di->setShared('session', function() {
+        $session = new \Phalcon\Session\Adapter\Files();
+        $session->start();
+        return $session;
+    });
+
+    // CSRF対策をディスパッチャーで実装
+    $di->set('dispatcher', function() use ($di) {
+        $eventsManager = new Phalcon\Events\Manager();
+        // beforeDispatchLoopにイベントを設定
+        $eventsManager->attach(
+            "dispatch:beforeDispatchLoop",
+            function($event, $dispatcher) use ($di) {
+                // POSTメソッドのみを対象に
+                if ($di->get('request')->getMethod() !== 'POST') {
+                    return;
+                }
+                // トークンをチェックして例外を投げる
+                if (! $di->get('security')->checkToken()) {
+                    throw new \Exception('Token error!');
+                }
+            }
+        );
+        // 例外を受けるためにbeforeExceptionにイベントを設定
+        $eventsManager->attach(
+            "dispatch:beforeException",
+            function($event, $dispatcher, $exception) {
+                // トークンエラーの例外の場合、そのままその例外を投げる
+                if ($exception->getMessage() === 'Token error!') {
+                    throw $exception;
+                }
+            }
+        );
+
+        $dispatcher = new Phalcon\Mvc\Dispatcher();
+        $dispatcher->setEventsManager($eventsManager);
+
+        return $dispatcher;
+    });
+
     //Setup the database service
     $di->set('db', function(){
         return new \Phalcon\Db\Adapter\Pdo\Mysql(array(
@@ -38,6 +79,7 @@ try {
     //Handle the request
     $application = new \Phalcon\Mvc\Application($di);
     echo $application->handle()->getContent();
-} catch(\Phalcon\Exception $e) {
-    echo "PhalconException: ", $e->getMessage();
+} catch(\Exception $e) {
+    // 最終的にすべての例外をここでキャッチ
+    echo "Exception: ", $e->getMessage();
 }

これで完了です。

解説

まず、CSRF対策ではセッションを使うため、セッションを自動的に開始するようにします。

    // CSRF対策にセッションを使うので自動でスタートするように
    $di->setShared('session', function() {
        $session = new \Phalcon\Session\Adapter\Files();
        $session->start();
        return $session;
    });

次に、トークンのチェックをディスパッチャーに実装します。

beforeDispatchLoopイベントで、POSTメソッドの場合だけトークンをチェックし例外を投げます。

例外が投げられるとbeforeExceptionイベントが発生するので、そこでトークンエラーの例外の場合、そのまま再度受けた例外を投げています。とりあえずExceptionクラスを使っていますが、実際には専用の例外クラスを作成するのがよいでしょう。

    // CSRF対策をディスパッチャーで実装
    $di->set('dispatcher', function() use ($di) {
        $eventsManager = new Phalcon\Events\Manager();
        // beforeDispatchLoopにイベントを設定
        $eventsManager->attach(
            "dispatch:beforeDispatchLoop",
            function($event, $dispatcher) use ($di) {
                // POSTメソッドのみを対象に
                if ($di->get('request')->getMethod() !== 'POST') {
                    return;
                }
                // トークンをチェックして例外を投げる
                if (! $di->get('security')->checkToken()) {
                    throw new \Exception('Token error!');
                }
            }
        );
        // 例外を受けるためにbeforeExceptionにイベントを設定
        $eventsManager->attach(
            "dispatch:beforeException",
            function($event, $dispatcher, $exception) {
                // トークンエラーの例外の場合、そのままその例外を投げる
                if ($exception->getMessage() === 'Token error!') {
                    throw $exception;
                }
            }
        );

        $dispatcher = new Phalcon\Mvc\Dispatcher();
        $dispatcher->setEventsManager($eventsManager);

        return $dispatcher;
    });

最終的に、トークンエラーの例外はindex.phpの最後のcatch句でキャッチされます。

…略…
} catch(\Exception $e) {
    // 最終的にすべての例外をここでキャッチ
    echo "Exception: ", $e->getMessage();
}

と、こんな感じでいいのかなと思いますが、まだあんまりよくわかってませんので、間違いがありましたらご指摘ください。

この記事はPhalcon Advent Calendar 2014の16日目です。明日は、yohgakiさんの「Phalcon PHPとSails Node.jsのベンチマーク」です。あと、誰かドキュメントの翻訳について書いてくださいまし。

関連

Date: 2014/12/16

Tags: phalcon, csrf