Phalcon \\ Mvc \\ Micro事件处理顺序和简单的CSRF对策


Phalcon是一位初学者,去年刚接触过它一次,在他的作品中经常使用Silex(或Pimple和Symfony组件)。

这就是为什么我考虑功能上类似于Silex的Phalcon \\\\ Mvc \\\\ Micro中事件处理顺序的原因,并且以此为基础,这是一个简单的CSRF对策。

已经确认可以工作的环境如下。

  • Windows 8
  • PHP 5.6.3
  • 菲尔康1.3.4

事件类型和处理顺序

以下内容定义为特定于微型应用程序的事件。

  • beforeHandleRoute
  • beforeExecuteRoute
  • beforeNotFound
  • afterExecuteRoute
  • afterHandleRoute

以下内容定义为中间件事件。

  • 之前

以下内容被定义为对未定义路由的请求的处理程序。

  • 未找到

验证来源1

使用以下来源验证处理订单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?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;
});

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

$manager = new \Phalcon\Events\Manager();

$manager->attach('micro', function($event, $app) {
    $app->logger->log($event->getType());
});

$app->setEventsManager($manager);

$app->before(function() use ($app) {
    $app->logger->log('before');
});

$app->after(function() use ($app) {
    $app->logger->log('after');
});

$app->finish(function() use ($app) {
    $app->logger->log('finish');
});

$app->notFound(function() use ($app) {
    $app->logger->log('notFound');
    $app->response->setStatusCode(404, 'Not Found');
    $app->response->send();
});

$app->get('/', function() use ($app) {
    $app->logger->log('index');
    return $app->response;
});

$app->get('/forward', function() use ($app) {
    $app->logger->log('forward');
    $app->handle('/forwarded');
});

$app->get('/forwarded', function() use ($app) {
    $app->logger->log('forwarded');
    return $app->response;
});

$app->handle();

路由成功时的处理顺序

如果由于路由到

请求而存在有效的处理程序,则将按以下顺序对其进行处理。

  • beforeHandleRoute事件
  • beforeExecuteRoute事件
  • 活动前
  • 处理程序执行
  • afterExecuteRoute事件
  • 事件发生后
  • afterHandleRoute事件
  • 记录GET / GET /验证源

    的时间

    1
    2
    3
    4
    5
    6
    7
    8
    [Sat, 20 Dec 14 22:09:04 +0900][DEBUG] beforeHandleRoute
    [Sat, 20 Dec 14 22:09:04 +0900][DEBUG] beforeExecuteRoute
    [Sat, 20 Dec 14 22:09:04 +0900][DEBUG] before
    [Sat, 20 Dec 14 22:09:04 +0900][DEBUG] index
    [Sat, 20 Dec 14 22:09:04 +0900][DEBUG] afterExecuteRoute
    [Sat, 20 Dec 14 22:09:04 +0900][DEBUG] after
    [Sat, 20 Dec 14 22:09:04 +0900][DEBUG] afterHandleRoute
    [Sat, 20 Dec 14 22:09:04 +0900][DEBUG] finish

    我没有遵循框架的内部处理,因此不再赘述,但是决定是在处理程序执行之前还是之后,执行之前或之后或之后是很重要的。

    路由错误时的处理顺序

    如果由于路由到请求而导致不存在有效的处理程序,则将按以下顺序对其进行处理。

  • beforeHandleRoute事件
  • beforeNotFound事件
  • notFound处理程序
  • 在验证源

    中GET /找不到时记录

    1
    2
    3
    [Sat, 20 Dec 14 22:10:11 +0900][DEBUG] beforeHandleRoute
    [Sat, 20 Dec 14 22:10:11 +0900][DEBUG] beforeNotFound
    [Sat, 20 Dec 14 22:10:11 +0900][DEBUG] notFound

    响应的状态代码是404 Not Found,因为

    notFound处理程序是$app->response->setStatusCode(404, 'Not Found');,然后是$app->response->send();

    (请注意,如果在没有send()的情况下使用return return $app->response;,则不会发送任何响应,并且结果将返回状态200。)

    与Silex等不同,Phalcon没有像错误处理程序那样的定义点,用于处理框架引发的异常。

    另外,在路由中,不会发生与\\" 405方法不允许\\"相对应的异常,并且通常会调用notFound处理程序。

    同样重要的是,在流程移至

    notFound处理程序之后,请勿在此之前,之后或完成之前调用任何方法,并且没有正常和错误所共有的后处理事件。

    出于安全原因,您可能希望为正常和错误设置一个公共响应标头,例如\\" X-Content-Type-Options:nosniff \\",但是唯一可以使用的事件是beforeHandleRoute。成为。

    如果未定义notFound处理程序,则在调用beforeHandleRoute→beforeNotFound事件之后,进程将以Fatal error: Uncaught exception 'Phalcon\Mvc\Micro\Exception' with message 'The Not-Found handler is not callable or is not defined' in...错误停止。

    从处理程序调用另一个处理程序时的处理顺序

    如果要将处理从

    处理程序转移到另一个处理程序(Silex(或Symfony?)中称为" sub-request"的函数),可以通过调用Phalcon\Mvc\Micro::handle()来实现。

    在这种情况下,将按以下顺序进行处理。

  • 第一个beforeHandleRoute事件
  • 第一个beforeExecuteRoute事件
  • 活动前1日
  • 第一个处理程序的执行
  • 第二个beforeHandleRoute事件
  • 第二个beforeExecuteRoute事件
  • 赛前第二
  • 第二个处理程序执行
  • 第二个afterExecuteRoute事件
  • 赛后第二
  • 第二次afterHandleRoute事件
  • 第一个afterExecuteRoute事件
  • 赛后第一
  • 第一个afterHandleRoute事件
  • 在GET /转发到验证源

    时记录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] beforeHandleRoute
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] beforeExecuteRoute
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] before
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] forward
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] beforeHandleRoute
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] beforeExecuteRoute
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] before
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] forwarded
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] afterExecuteRoute
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] after
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] afterHandleRoute
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] finish
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] afterExecuteRoute
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] after
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] afterHandleRoute
    [Sat, 20 Dec 14 22:27:11 +0900][DEBUG] finish

    每次使用

    Phalcon\Mvc\Micro::handle()调用另一个处理程序时,将重复调用beforeHandleRoute→beforeExecuteRoute→before→处理程序执行→afterExecuteRoute→after→afterHandleRoute→finish。

    在某些情况下,仅当第一个事件发生在请求后立即发生或最后一个事件发生在发送响应之前即发生,但是就读取API文档而言,才支持执行此功能作为框架,似乎还没有完成。

    顺便说一句,如果您尝试从notFound处理程序中执行此操作怎么办?

    重写如下...

    1
    2
    3
    4
    5
    6
    <?php
    $app->notFound(function () use ($app) {
        $app->logger->log('notFound');
        $app->response->setStatusCode(404, 'Not Found');
        $app->handle('/forwarded');
    });

    在验证源

    中GET /找不到时记录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [Sat, 20 Dec 14 22:35:22 +0900][DEBUG] beforeHandleRoute
    [Sat, 20 Dec 14 22:35:22 +0900][DEBUG] beforeNotFound
    [Sat, 20 Dec 14 22:35:22 +0900][DEBUG] notFound
    [Sat, 20 Dec 14 22:35:22 +0900][DEBUG] beforeHandleRoute
    [Sat, 20 Dec 14 22:35:22 +0900][DEBUG] beforeExecuteRoute
    [Sat, 20 Dec 14 22:35:22 +0900][DEBUG] before
    [Sat, 20 Dec 14 22:35:22 +0900][DEBUG] forwarded
    [Sat, 20 Dec 14 22:35:22 +0900][DEBUG] afterExecuteRoute
    [Sat, 20 Dec 14 22:35:22 +0900][DEBUG] after
    [Sat, 20 Dec 14 22:35:22 +0900][DEBUG] afterHandleRoute
    [Sat, 20 Dec 14 22:35:22 +0900][DEBUG] finish

    响应的状态代码将为404 Not Found。

    是否符合预期?

    使用Phalcon \\\\ Security和output_add_rewrite_var()

    的简单CSRF措施

    确认

    事件的处理顺序后,请尝试使用Phalcon \\\\ Security引入CSRF对策。

    查看

    API文档,它看起来像一个用于安全性的实用程序类,带有散列,密码和生成随机数的方法。 (其中一些是静态方法)

    • 类Phalcon \\\\安全性— Phalcon 1.3.1文档

    DI生成就是这样。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?php
    $di = new \Phalcon\DI();

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

    // …中略…

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

    Phalcon \\\\安全实现Phalcon \\\\ DI \\\\ InjectionAwareInterface,因此完成了setDI()。

    将以下方法用于CSRF度量。

    • Phalcon \\\\ Security :: getTokenKey()返回CSRF令牌的名称
    • Phalcon \\\\ Security :: getToken()返回CSRF令牌的值
    • Phalcon \\\\ Security :: getSessionToken()返回由getToken()从会话中生成的CSRF令牌的值
    • Phalcon \\\\ Security :: checkToken()验证由getTokenKey()和getToken()生成的CSRF令牌的名称和值

    从这里开始,我将使其变得"简单",但让我们使用output_add_rewrite_var()。

    使用此功能,将自动启用输出缓冲,并且" URL-Rewriter"处理程序将透明地重写HTML,以便A标记的HREF属性值具有任意名称和值。在FORM标记中具有任何名称和值的字段。换句话说,您无需触摸HTML就可以输出CSRF令牌。

    (这等效于session.use_trans_sid,我现在很少使用。)

    另外,通过在执行处理程序之前在事件中执行CSRF令牌检查,可以在不修改处理程序端代码的情况下实现它。

    再次查看正常的处理顺序...

  • beforeHandleRoute事件
  • beforeExecuteRoute事件
  • 活动前
  • 处理程序
  • afterExecuteRoute事件
  • 事件发生后
  • afterHandleRoute事件
  • 用于POST请求的CSRF令牌检查在早期似乎不错,但是由于output_add_rewrite_var()进行的标记重写使用PHP输出缓冲,因此在启用输出缓冲之前会发送响应。工作。

    这次,我假设响应未在要启用CSRF令牌的屏幕上的处理程序中发送,因此编写了以下内容。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?php
    $manager = new \Phalcon\Events\Manager();

    $manager->attach('micro:beforeHandleRoute', function($event, $app) {
        if ($app->request->getMethod() === 'POST') {
            if ($app->security->checkToken() === false) {
                throw new \RuntimeException('Invalid Request');
            }
        }
    });

    $manager->attach('micro:afterHandleRoute', function($event, $app) {
        ini_set('url_rewriter.tags', 'form=');
        output_add_rewrite_var($app->security->getTokenKey(), $app->security->getToken());
    });

    $app->setEventsManager($manager);

    这是流程。

  • beforeHandleRoute事件…检查POST发送的CSRF令牌
  • beforeExecuteRoute事件
  • 活动前
  • 处理程序
  • afterExecuteRoute事件
  • 事件发生后
  • afterHandleRoute事件...向表单添加(指定)CSRF令牌的隐藏字段
  • 在第一个beforeHandleRoute中,如果POST请求和CSRF令牌不匹配,则引发异常。

    (实际上,我认为它将捕获异常并返回错误屏幕,但这次我将其省略。)

    在最后一个HandleRoute之后,output_add_rewrite_var()输出CSRF令牌。

    要重写的标记的类型由url_rewriter.tags决定,但是ini_set()用于缩小范围,仅形成形式,以便不重写不必要的标记。

    验证来源2

    使用以下来源验证处理结果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    <?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('session', function() {
        $session = new \Phalcon\Session\Adapter\Files();
        $session->start();
        return $session;
    });

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

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

    $manager = new \Phalcon\Events\Manager();

    $manager->attach('micro:beforeHandleRoute', function($event, $app) {
        $app->logger->log($event->getType());
        if ($app->request->getMethod() === 'POST') {
            if ($app->security->checkToken() === false) {
                $app->logger->log('checkToken() NG');
                throw new \RuntimeException('Invalid Request');
            }
            $app->logger->log('checkToken() OK');
        }
    });

    $manager->attach('micro:afterHandleRoute', function($event, $app) {
        $app->logger->log($event->getType());
        $name = $app->security->getTokenKey();
        $value = $app->security->getToken();
        ini_set('url_rewriter.tags', 'form=');
        output_add_rewrite_var($name, $value);
        $app->logger->log(sprintf('output_add_rewrite_var("%s", "%s")', $name, $value));
    });

    $app->setEventsManager($manager);

    $app->map('/', function () use ($app) {
        if ($app->request->getMethod() === 'POST') {
            $app->logger->log('POST /');
        } else {
            $app->logger->log('GET /');
        }
        return $app->response->setContent(<<<HTML
    <html>
    <body>
    <form method="post" action="/">
    <input type="submit" value="POST" />
    </form>
    </body>
    </html>
    HTML
        );
    })->via(['GET', 'POST']);

    $app->handle();

    显示表格

    时的处理

    启用CSRF令牌检查和插入并尝试显示表单。

    记录GET / GET /验证源

    的时间

    1
    2
    3
    4
    [Mon, 22 Dec 14 12:54:52 +0900][DEBUG] beforeHandleRoute
    [Mon, 22 Dec 14 12:54:52 +0900][DEBUG] GET /
    [Mon, 22 Dec 14 12:54:52 +0900][DEBUG] afterHandleRoute
    [Mon, 22 Dec 14 12:54:52 +0900][DEBUG] output_add_rewrite_var("ZwF1hqL3MVzzBEtf", "6300d95a7766663ab9fe363f01074a84")

    响应HTML

    1
    2
    3
    4
    5
    6
    7
    <html>
    <body>
    <form method="post" action="/"><input type="hidden" name="ZwF1hqL3MVzzBEtf" value="6300d95a7766663ab9fe363f01074a84" />
    <input type="submit" value="POST" />
    </form>
    </body>
    </html>

    像这样在表单屏幕的HTML中插入隐藏字段的结果是,在下一次POST传输中将执行CSRF令牌检查。

    提交表格

    时的处理

    尝试提交带有CSRF令牌的表单。

    从显示的表单

    发送POST时记录

    1
    2
    3
    4
    5
    [Mon, 22 Dec 14 12:55:27 +0900][DEBUG] beforeHandleRoute
    [Mon, 22 Dec 14 12:55:27 +0900][DEBUG] checkToken() OK
    [Mon, 22 Dec 14 12:55:27 +0900][DEBUG] POST /
    [Mon, 22 Dec 14 12:55:27 +0900][DEBUG] afterHandleRoute
    [Mon, 22 Dec 14 12:55:27 +0900][DEBUG] output_add_rewrite_var("S19KusqTLFr01PoO", "f1fbeee3dfee1ed2d2c2c2c47420601d")

    您可以看到CSRF令牌检查已完成,并且新的令牌密钥和值已再次发出。

    验证源示例中,接受POST发送后,没有重定向(所谓的" PRG模式"),因此,如果按" F5"键重新发送,CSRF令牌检查将运行并失败。

    完成POST后使用" F5"键重新发送时记录

    1
    2
    [Sat, 22 Dec 14 12:56:12 +0900][DEBUG] beforeHandleRoute
    [Sat, 22 Dec 14 12:56:12 +0900][DEBUG] checkToken() NG

    由于这次不执行异常处理,因此处理将因错误Fatal error: Uncaught exception 'RuntimeException' with message 'Invalid Request' in...而停止。

    (PHP默认错误处理程序返回响应,因此状态代码为200。)

    作为CSRF措施的一次性令牌发行

    这样,Phalcon \\\\ Security的CSRF令牌是一个一次性令牌,因此,如果您在多个选项卡中打开表单,除最后打开的表单外,它将被判定为无效。

    使用诸如

    Opera(Blink)之类的浏览器在显示源时显示源之后提交表单后,同样适用。

    如果您希望在支持多个选项卡后拥有一个带有到期日期的令牌,那么看来您现在必须依靠某些其他库或实现自己的库。

    • 单次检查令牌是否无效? (一次性令牌)相同的值在任何时候被销毁之前是否有效? (固定令牌)
    • 如果在同一会话中打开多个选项卡该怎么办?你犯了一个错误吗?每个过渡都是分支吗? (在后一种情况下,是否有必要限制最大分支数?)
    • 如何将令牌值从服务器传递到客户端,以及如何将其从客户端发送到服务器?

    无论是自己实现还是使用现有库,都需要考虑此处的要求。

    我想将其视为下一个问题。

    参考链接

    有关如何通过Phalcon \\\\ Security实施CSRF措施,请参阅本文。

    • 与Phalcon-Qiita一起采取CSRF措施
    • 将CSRF措施添加到Phalcon教程(教程1)中-宁静的一天(重新加载)-PHP,FuelPHP,Linux或其他

    我之前在我的博客上通过output_add_rewrite_var()撰写了有关CSRF措施的文章。

    • 使用output_add_rewrite_var()尝试CSRF措施

    本文将帮助您了解CSRF措施的基本概念。

    • 针对开发人员的正确CSRF措施

    文章中所述,关于如何使用会话ID及其哈希值,现在有很多负面意见,它们通常被称为"固定令牌"。

    此外,以下文章已被警告,前提是HTML源将被泄漏。

    • 首先,没有时间将会话ID本身用作CSRF对策令牌的值。

    另外,尽管这是一篇过时的文章,但我认为这也是一本必读的文章。

    • 高木裕光@家庭日记-为什么不建议CSRF措施使用"一次性令牌"方法,隐藏参数是否容易泄漏?

    相关链接和要点在此处井井有条。

    • 技术/安全/ CSRF对策参考备忘录--Glamenv-Septzen.net