跨站点请求伪造（CSRF）及对策


今天，微博里流传着“搜狗输入法泄密事件”。
安全问题，一直以来都是关系到企业生存的重要问题，然而真正把安全问题提到重要位置的公司又有几家？据乌云漏洞的官方说法，搜狗输入法这个问题早在5月就已经提交给厂商了，然后，这一问题并没被搜狗加以重视，以至于现在在网上掀起了又一个风波。

不说公司，我们程序员在写代码的时候，又有多少人想到过如何处理安全问题？在这里，我们就来普及以下CSRF的基本知识及相关预防措施。



# 1. 什么是CSRF攻击

CSRF是Cross site request forgeries的首字母缩写，有时候也有人写作XSRF。
这也是和XSS，SQL注入等并列的比较常见的Web开发中的安全问题。

简单来说，就是用户在浏览a.com站点的时候，里面有一些恶意内容，在暗地里访问了另一个站点b.com的内容，从而在用户不知情的情况下做出了一些恶意操作。

# 2. 攻击原理

就其技术背景来说，基本上攻击目标都是需要用户登录认证后面受保护的内容，基本上都是通过cookie和session来完成操作的。

其基本攻击原理见下图：

![csrf](http://liubin.org/wp-content/uploads/2013/06/28232804-51bdd6fb20674c99a3af90124348859c.png)

首先，用户先按正常流程访问某一服务，并保持登录状态；
之后，在第4步的时候，用户通过邮件或者IM等，收到了具有恶意代码的攻击网址，当用户打开此攻击页面时，攻击者通过利用img，script，iframe等元素的src属性，来在用户不知情的情况下，发起对正常site的恶意访问。在图中第5步时，恶意代码已经执行完毕，完成了攻击工作。


# 3. 防范措施

## 3.1. 基本防范措施

## 3.1.1. 对可能成为恶意代码寄存元素的检查

比如，尽量不让用户能自由定制带有javascript和iframe的内容；
对于用户可以上传图片，内容编辑的功能，对需要显示图片的img元素，确保对src属性进行验证。

### 3.1.2. 使用Cookie的HttpOnly属性

通过使用这个属性，禁止客户端脚本来访问Cookie，从而保证数据的安全。


### 3.1.3. 检查Referer

通过检查http header里的Referer来确认请求是否合法。
不过这个方法可能会导致编码变得复杂，因为我们必须对整个系统的所有请求链进行程序化。最严重的问题是，存在有可能会取不到Referer的情况。

### 3.1.4. anti CSRF token
这个方法一般被认为是最实用，且也是应用最广泛的一种方法。
简单来说，一般是在session里保存一个随机的不能被猜测到的token，同时，在客户端的form里 ，通过hidden元素，写到客户端页面。当客户端提交的时候，服务器端可以通过此提交的token来和session里存放的token来对比，通过其对比结果来判断客户端的访问是否合法。

需要注意的是，一般的语言或者framework一般只支持html的token，如果用flash等技术，需要把这些信息也要传递给flash等，使得flash也能将这些数据，包括token通过POST提交到服务器。
需要注意的是，这个token不能（通过cookie等方式）存放在客户端。


### 3.1.5. 尽量使用POST，放弃GET
CSRF攻击多数攻击代码都被隐藏在iframe，img等元素的src属性里，这样浏览器进行加载的时候，只会发起get操作，所以，如果我们的服务器端都改为POST的话，至少会降低被攻击的概率。

比如，在页面上，比如删除等链接，不能直接使用类似*.php?action=delete&id=534等这样的方法，而应该改为Form来提交。

而且，这样对后端代码书写也有要求，也存在一些陷阱。比如在PHP里，针对POST的提交，需要只从$_POST取数据，如果从$_REQUEST里面取数据的话，是不能区分用户是通过GET还是POST来提交的，需要先判断客户提交方法。

如果使用一些framework的话，可能通过routing机制已经对GET/POST和处理方法做了一一对应。

具体的使用例子请参考3.2. 和 3.3. 章内容。

### 3.1.6. 增加确认页面
在进行删除，更新处理的时候，我们不是直接进行数据操作，而是插入一些确认画面、对话框之类的页面，然后再确认的时候进行token之类的设置，最后再提交给服务器。

从本质上将，本条应该还是属于anti CSRF token的对策范围。

### 3.1.7. 密码确认

对于任何会修改数据的操作都做再输入密码确认确实是对用户来说比较麻烦，但对于关键的信息，比如email，密码等内容的修改，则可以考虑假如密码确认步骤。

### 3.1.8. 验证码（CAPTCHA）

验证码，这个不光能防止CSRF，还能防止SPAM。

### 3.1.9. 通过Ajax预防CSRF

除了上述方法，还可以通过Ajax来和服务器通信来减少CSRF的风险。这时候可以考虑在请求的header里加入页面信息来做合法性验证。比如参考如下代码：


```javascript
function post(){

    var xhr = new XMLHttpRequest();
    xhr.open( "POST", "/create", true );
    …….
    // 在xhr.open方法后，进行header设置。
    xhr.setRequestHeader( "X-From", location.href );
    ……
    xhr.send( s );
    return false;
}

```

服务器端在接收到客户端的请求后，会进行如下处理：

1. 检查header里的Host是否正确。
2. 检查是否设置了“X-From”属性，是否是正确的page url的值。
3. 检查Origin header属性，这个值可以是：
  没有设置；
  或者：如果设置了，则应该和X-From一样。

如果请求满足了所有这些条件，都可以认为是正常的访问。

如果攻击者如果直接通过假页面提交Form，则不能在header里设置X-From属性；而如果攻击者也用Ajax提交，则header里的Origin因为不能伪造，也不能达到攻击的目的。从而达到了预防CSRF的目的。


### 3.1.10. 预防XSS漏洞

基本上，如果系统出现了XSS漏洞的话，基本CSRF攻击就很容易被植入了。


## 3.2. rails下的防范措施

rails下可以在Controller里面调用protect_from_forgery：

```ruby
class ApplicationController < ActionController::Base
 
  protect_from_forgery

  # or
  protect_from_forgery :except => ["create"]

  # or
  protect_from_forgery :only => ["create"]

 
end
```

token验证失败的时候，会执行reset_session操作。


## 3.2. php下的防范措施

貌似PHP官方版本还没有直接对此提供支持，可以自己实现。
如果用Symfony2的话，可以使用**CsrfProviderInterface**，这个类提供了两个方法，**generateCsrfToken**用来生产token，** isCsrfTokenValid**用来验证请求中附带的token是否合法：

```php
//Symfony\Component\Form\Extension\Csrf\CsrfProvider\SessionCsrfProvider by default
$csrf = $this->get('form.csrf_provider'); 

//Intention should be empty string, if you did not define it in parameters
$token = $csrf->generateCsrfToken($intention); 

return new Response($token);
```
具体使用说明参考[这里](http://api.symfony.com/master/Symfony/Component/Form/Extension/Csrf/CsrfProvider/CsrfProviderInterface.html)。


# 4. 总结

总之，安全问题在平时可能不会太引人注意，但是一旦出现问题，损失的就不光是当前的金钱利益了，对于企业形象，将来发展，都有具体的影响。
对于我们程序员来说，也要对自己的代码安全负责。
这里推荐一本 《白帽子讲Web安全》，内容非常丰富，也很容易理解。建议所有做web的同学都读一下。

---

参考文档

1. http://d.hatena.ne.jp/hasegawayosuke/20130302/p1

2. https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)