在讲解CSRF攻击原理及流程之前,我想先花点时间讲讲浏览器信息传递中的Session机制。
Session机制
Session,中文意思是“会话”。对于“会话”我的理解是客户端与服务端间通信的一种方式,也可以简单的理解为一个用户从打开浏览器开始,访问一个web网站,点击某些超链接,访问某些服务端的资源,然后关闭浏览器的这一整个过程就是一次会话。
早期,客户端与服务端之间的每次信息传递都是独立的。这与HTTP协议的无状态性有关。用户发送的一个请求只是为了告诉服务端想访问的资源,然后服务端将用户要的资源返回回去,就这么简单。后来,随着人们需求的增长,网站的所有者希望对每个用户提供个性的、精细化的服务,最初的静态资源已经无法满足如“用户机制”、“个性推荐”等多样的需求了。
对于无状态的HTTP协议,人们提出来两种解决方案,分别是Cookie和Session。下面讲一下Cookie和Session的区别及联系。
Cookie机制:一般来说,Cookie分发是通过扩展HTTP协议来实现的,服务器通过在HTTP的响应头中加上一行特殊的指示以提示浏览器按照指示生成相应的Cookie。然而纯粹的客户端脚本如JavaScript也可以生成Cookie。Cookie相当于由用户自己保存的一张纸,上面记载着用户的信息。比如用户名、密码等等。Cookie一般是由浏览器在后台自动发送给服务器的。浏览器会检查所有的Cookie,当某个Cookie的作用域大于或等于所要访问的资源的位置时,浏览器就会把这个Cookie附在请求资源的HTTP请求头上发送给服务器。可以说,这种方式是客户端(用户)在维持状态。
Session机制:客户端请求服务端时,服务端会为客户端创建一个Session,并检查请求中是否包含Session ID。形象的来说,一个Session相当于是一张会员卡,上面除了一个卡号其他什么都没有。这个卡号就是Session ID。当存在Session ID时就检索出相应的Session。不存在则创建一个Session并生成一个Session ID。Session ID的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串。当一个用户拿着这张“会员卡”访问一个网站时,用户在网站上的有关信息和操作都会被记录在服务端的这张会员卡对应的卡号下。很明显,这种方式就是服务端在维持状态。而Session机制和Cookie机制又有什么联系呢?虽然Session机制中用户的状态由服务端来维持,但是,Session中的Session ID还是要用户自己来保管的,而一般来说,Session ID则以Cookie的形式保存在客户端。但这种方式有一个弊端就是如果客户端禁用了Cookie,那么Session机制将无法正常工作。解决这个问题有两种方法,一种是URL重写,简单的说就是将Session ID作为URL的附加信息或参数,通过URL来传递。另一种是将Session ID写在表单(Form)的隐藏域中,在表单提交时将Session ID一起提交上去。
CSRF攻击
CSRF(Cross-site request Forgery)攻击称为跨站请求伪造攻击。听起来和XSS(跨站脚本攻击)有些类似,但是实际上完全不同。我们来看一下这两者的区别。
XSS:构造代码 → 伪装代码 → 发送给受害者 → 受害者打开 → 攻击者获取受害者的Cookie → 攻击者使用受害者的Cookie去干坏事 → 攻击完成
CSRF:构造代码 → 伪装代码 → 发送给受害者 → 受害者打开 → 受害者执行了恶意代码 → 攻击完成
可以发现,XSS是攻击者获取到了受害者的Cookie,自己去执行恶意代码操作,而CSRF则是受害者在打开攻击者的代码时,攻击就已经完成了,攻击者只需构造代码并诱使受害者打开,一次CSRF攻击就完成了。简单的说,XSS是盗取Cookie,CSRF是盗用Session。
下面我们用一张图来说明一次完整的CSRF攻击:
1.首先,用户访问并登录可信站点A,可以是某后台登录系统,也可以是某购物网站或者某网上银行;
2.网站A验证用户为合法用户,验证成功,并在用户处产生Cookie;
3.用户在没有登出网站A的情况下,访问了危险网站B,危险网站B一般为攻击者用来进行CSRF攻击而制作的网站;
4.危险网站B要求访问A,并发送请求,这里的请求可能是恶意代码(注意:此时用户在网站A仍处于登录状态);
5.浏览器根据B的请求,带着A的Cookie向A发送了请求,也就是说冒充了A的身份执行了攻击者想执行的恶意代码;
从上面的五个步骤来看,完成一次完整的CSRF攻击需要两个条件:
1.用户登录可信站点A,并在本地存储了A的Cookie;
2.用户在不登出A的情况下,访问B;
两个实例
1.假设有一个后台管理页面A,管理员可以在上面新增用户。以GET请求方式来完成操作比如 http://www.a.com/admin/adduser.php?username=abc&password=123
这个请求就是请求服务端添加一个用户名为abc,密码为123的用户。
当然了,这个操作必须在管理员登录后台成功后才能执行现在管理员在后台保持登录状态的时候,访问了一个网站B,其中有这样一段代码:如<img src=http://www.a.com/admin/adduser.php?username=abc&password=123>
访问了之后,在管理员的后台管理中就会自动添加上一个用户名为abc,密码为123的用户。
原理就是浏览器自动带上了验证后的A的Cookie,发送了危险网站B的请求。服务端误以为是管理员自己发出的请求,便在服务端执行了添加用户的操作。
2.由于GET方式的不安全性,后台管理系统进行了升级,使用POST请求方式。添加用户的页面变成了POST表单:
处理POST表单的服务端代码如下:
看似安全了,其实仍有办法进行CSRF攻击。危险站点B的代码如下:
同样,管理员在A站点登录时,访问了站点B,那么在后台同样也会新增了一个用户名为abc,密码为123的用户。
只不过在A站点使用了POST提交数据后,B也要使用表单来提交数据,相对麻烦一点。
CSRF的防御
1. 检查HTTP Referer字段是否同域HTTP Referer 是header的一部分,当浏览器向服务端发送请求时,浏览器会带上Referer,用于告诉服务端请求的来源。一般来说,用户提交的站内请求的来源(也就是Referer字段)应该站内地址,当检测到非同域时,有理由怀疑用户受到了CSRF攻击。虽然这种方法简单又有效,但是!我觉得这种方法目前已经变得不是那么可靠了。原因有三: (1) 这种方法只能防御来自站外的CSRF,却无法防御来自站内的CSRF;(2) 当从HTTPS站点发送请求到HTTP站点时,浏览器不发送Referer,即无法检测请求来源;(3) 虽然JavaScript/ActionScript无法修改Referer,但是Referer可以在服务端被伪造,即可以被向可信站点A发送请求的危险站点B伪造,从而通过检查机制;
2. 使用验证码很易于理解,就是在用户进行操作时让用户输入验证码,确保是用户本人进行的操作,而不是第三方。然而这种方式会降低用户的使用体验,给用户带来不便;
3. 限制Session Cookie的生存周期即规定如果用户在一段时间内不进行任何操作,服务端就自动销毁Session,用户再次操作时需要重新登录才能继续操作。因为无法真正做到用户一关闭浏览器服务端就销毁Session,虽然可以在用户关闭浏览器时给服务端发送一个销毁Session的请求,但是当浏览器崩溃或被强制关闭时,销毁Session的请求无法发出,服务端就一直会保持着这个Session;
4. 使用一次性Token这种方法可以说是目前最广泛使用的解决方案了。这里的Token是一个由数字、字母组成的随机值,每次生成的Token必须具有唯一性且不易被猜测到。在用户登录后,服务端会生成一个一次性的Token,一般这个Token会保存在服务端返回给用户的页面中的一个隐藏域里。每次用户向服务端发送操作请求时会附带上这个Token,服务端也会验证这个Token是否和分发给用户的Token一致,如果请求中不存在Token或Token不正确,即判定这个请求为非法请求。这个解决方案的原理就是利用了浏览器的同源策略,即第三方无法通过AJAX等方式获取到Token值。当然了,显而易见这个Token不具备时效性。我们可以使用一个临时的作用在父子页面之间的Cookie来代替Token。