用令牌保护表格的安全

这个事例介绍了另一种非常简单的技术,它可以保护你的表单免受跨站点请求伪造(CSRF)攻击。简单地说,当攻击者可能使用其他技术感染您网站上的一个网页时,CSRF攻击是可能的。在大多数情况下,受感染的网页会使用有效的登录用户的凭证开始发出请求(即使用JavaScript购买商品,或进行设置更改)。你的应用程序要检测到这种活动是非常困难的。一个可以轻松采取的措施是生成一个随机令牌,包含在每个要提交的表单中。由于受感染的页面将无法访问该令牌,也无法生成匹配的令牌,因此表单验证将失败。

如何做…

1.首先,为了演示这个问题,我们创建一个网页,模拟一个受感染的页面,生成一个请求,向数据库发布一个条目。在这个说明中,我们将调用文件chap_12_form_csrf_test_unprotected.html

  1. <!DOCTYPE html>
  2. <body onload="load()">
  3. <form action="/chap_12_form_unprotected.php"
  4. method="post" id="csrf_test" name="csrf_test">
  5. <input name="name" type="hidden" value="No Goodnick" />
  6. <input name="email" type="hidden" value="malicious@owasp.org" />
  7. <input name="comments" type="hidden"
  8. value="Form is vulnerable to CSRF attacks!" />
  9. <input name="process" type="hidden" value="1" />
  10. </form>
  11. <script>
  12. function load() { document.forms['csrf_test'].submit(); }
  13. </script>
  14. </body>
  15. </html>
  1. 接下来,我们创建一个名为chap_12_form_unprotected.php的脚本来响应表单发布。与本书中的其他调用程序一样,我们设置了自动加载,并使用第5章 “与数据库的交互 “中所涉及的Application\Database\Connection类。
  1. <?php
  2. define('DB_CONFIG_FILE', '/../config/db.config.php');
  3. require __DIR__ . '/../Application/Autoload/Loader.php';
  4. Application\Autoload\Loader::init(__DIR__ . '/..');
  5. use Application\Database\Connection;
  6. $conn = new Connection(include __DIR__ . DB_CONFIG_FILE);
  1. 然后,我们检查进程按钮是否被按下,甚至实现过滤机制,这在本章的过滤$_POST数据事例中有所涉及。这是为了证明CSRF攻击很容易绕过过滤器。
  1. if ($_POST['process']) {
  2. $filter = [
  3. 'trim' => function ($item) { return trim($item); },
  4. 'email' => function ($item) {
  5. return filter_var($item, FILTER_SANITIZE_EMAIL); },
  6. 'length' => function ($item, $length) {
  7. return substr($item, 0, $length); },
  8. 'stripTags' => function ($item) {
  9. return strip_tags($item); },
  10. ];
  11. $assignments = [
  12. '*' => ['trim' => NULL, 'stripTags' => NULL],
  13. 'email' => ['length' => 249, 'email' => NULL],
  14. 'name' => ['length' => 128],
  15. 'comments'=> ['length' => 249],
  16. ];
  17. $data = $_POST;
  18. foreach ($data as $field => $item) {
  19. foreach ($assignments['*'] as $key => $option) {
  20. $item = $filter[$key]($item, $option);
  21. }
  22. if (isset($assignments[$field])) {
  23. foreach ($assignments[$field] as $key => $option) {
  24. $item = $filter[$key]($item, $option);
  25. }
  26. $filteredData[$field] = $item;
  27. }
  28. }
  1. 最后,我们使用准备好的语句将过滤后的数据插入到数据库中。然后我们重定向到另一个脚本,叫做chap_12_form_view_results.php,它只是简单地转储visitors表的内容。
  1. try {
  2. $filteredData['visit_date'] = date('Y-m-d H:i:s');
  3. $sql = 'INSERT INTO visitors '
  4. . ' (email,name,comments,visit_date) '
  5. . 'VALUES (:email,:name,:comments,:visit_date)';
  6. $insertStmt = $conn->pdo->prepare($sql);
  7. $insertStmt->execute($filteredData);
  8. } catch (PDOException $e) {
  9. echo $e->getMessage();
  10. }
  11. }
  12. header('Location: /chap_12_form_view_results.php');
  13. exit;
  1. 结果当然是允许攻击,尽管有过滤和使用准备好的语句。

  2. 实现表单保护令牌其实很简单!首先,你需要生成令牌并存储在会话中。首先,你需要生成令牌并将其存储在会话中。我们利用新的random_bytes() PHP 7 函数来生成一个真正的随机令牌,这个令牌很难,甚至不可能被攻击者匹配。

  1. session_start();
  2. $token = urlencode(base64_encode((random_bytes(32))));
  3. $_SESSION['token'] = $token;

{% hint style=”info” %} random_bytes()的输出是二进制的,我们使用base64_encode()将其转换为可用的字符串。我们使用base64_encode()将其转换为可用的字符串。然后我们使用urlencode()进一步处理它,使它正确地呈现在HTML表格中。 {% endhint %}

  1. 当我们渲染表单时,我们将标记作为一个隐藏的字段显示出来。
  1. <input type="hidden" name="token" value="<?= $token ?>" />
  1. 然后我们复制并修改之前提到的chap_12_form_unprotected.php脚本,添加逻辑来检查是否与存储在会话中的token匹配。请注意,我们取消设置当前的token,使其在将来使用时无效。我们调用新脚本 chap_12_form_protected_with_token.php
  1. if ($_POST['process']) {
  2. $sessToken = $_SESSION['token'] ?? 1;
  3. $postToken = $_POST['token'] ?? 2;
  4. unset($_SESSION['token']);
  5. if ($sessToken != $postToken) {
  6. $_SESSION['message'] = 'ERROR: token mismatch';
  7. } else {
  8. $_SESSION['message'] = 'SUCCESS: form processed';
  9. // continue with form processing
  10. }
  11. }

如何运行…

要测试受感染的网页如何发起CSRF攻击,请创建以下文件,如前面的事例所示。

  • chap_12_form_csrf_test_unprotected.html
  • chap_12_form_unprotected.php

然后你可以定义一个名为chap_12_form_view_results.php的文件,用来转储visitors表。

  1. <?php
  2. session_start();
  3. define('DB_CONFIG_FILE', '/../config/db.config.php');
  4. require __DIR__ . '/../Application/Autoload/Loader.php';
  5. Application\Autoload\Loader::init(__DIR__ . '/..');
  6. use Application\Database\Connection;
  7. $conn = new Connection(include __DIR__ . DB_CONFIG_FILE);
  8. $message = $_SESSION['message'] ?? '';
  9. unset($_SESSION['message']);
  10. $stmt = $conn->pdo->query('SELECT * FROM visitors');
  11. ?>
  12. <!DOCTYPE html>
  13. <body>
  14. <div class="container">
  15. <h1>CSRF Protection</h1>
  16. <h3>Visitors Table</h3>
  17. <?php while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) : ?>
  18. <pre><?php echo implode(':', $row); ?></pre>
  19. <?php endwhile; ?>
  20. <?php if ($message) : ?>
  21. <b><?= $message; ?></b>
  22. <?php endif; ?>
  23. </div>
  24. </body>
  25. </html>

在浏览器中,启动chap_12_form_csrf_test_unprotected.html。以下是输出结果的显示方式。

用令牌保护表格的安全 - 图1

正如你所看到的,尽管有过滤和使用准备好的声明,攻击还是成功了!

接下来,将chap_12_form_unprotected.php文件复制到chap_12_form_protected.php中。进行事例中第8步的修改。还需要修改测试HTML文件,将chap_12_form_csrf_test_unprotected.html复制到chap_12_form_csrf_test_protected.html。将FORM标签中的action参数的值修改如下。

  1. <form action="/chap_12_form_protected_with_token.php"
  2. method="post" id="csrf_test" name="csrf_test">

当你在浏览器中运行新的HTML文件时,它会调用chap_12_form_protected.php,寻找一个不存在的标记。这里是预期的输出。

用令牌保护表格的安全 - 图2

最后,继续定义一个名为chap_12_form_protected.php的文件,生成一个token,并将其显示为一个隐藏的元素。

  1. <?php
  2. session_start();
  3. $token = urlencode(base64_encode((random_bytes(32))));
  4. $_SESSION['token'] = $token;
  5. ?>
  6. <!DOCTYPE html>
  7. <body onload="load()">
  8. <div class="container">
  9. <h1>CSRF Protected Form</h1>
  10. <form action="/chap_12_form_protected_with_token.php"
  11. method="post" id="csrf_test" name="csrf_test">
  12. <table>
  13. <tr><th>Name</th><td><input name="name" type="text" /></td></tr>
  14. <tr><th>Email</th><td><input name="email" type="text" /></td></tr>
  15. <tr><th>Comments</th><td>
  16. <input name="comments" type="textarea" rows=4 cols=80 />
  17. </td></tr>
  18. <tr><th>&nbsp;</th><td>
  19. <input name="process" type="submit" value="Process" />
  20. </td></tr>
  21. </table>
  22. <input type="hidden" name="token" value="<?= $token ?>" />
  23. </form>
  24. <a href="/chap_12_form_view_results.php">
  25. CLICK HERE</a> to view results
  26. </div>
  27. </body>
  28. </html>

当我们从表单中显示并提交数据时,令牌会被验证,并允许继续插入数据,如图所示。

用令牌保护表格的安全 - 图3

更多…

有关CSFR攻击的更多信息,请参考https://www.owasp.org/index.php/Cross-Site\_Request\_Forgery\_\(CSRF\)。