跨过山和大海,伪装成熟悉的陌生人

什么CSRF

CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。

有何危害

故事要从三天前说起,你是一个月薪3000的程序员,好不容易存够了20000打算更新自己的主机。

这天你在网吧上网,想抽烟(抽烟有害健康)。于是你登陆账户,打算从里面取100大洋。与此同时,一个网页吸引你的注意,硕大的“屠龙宝刀,点击就送”使你情不自禁地点击。点进去以后,你发现跳出的居然是转账页面。定睛一看,转入的居然还不是你的账号,金额是一万。你两眼发昏,陷入沉思。

为什么会发生这样的情况呢?我们来看看后台发生了什么:

首先,你登陆进去以后,cookie里会储存一个你的登陆信息,告诉服务器,你登陆了。

from flask import Flask
from flask import redirect, request, session, url_for

app = Flask(__name__)
app.secret_key = 'APPLE_suck5'

@app.route('/')
def hello():
    return 'Hello World'

@app.route('/login')
def login():
    session['logged_in'] = True
    return redirect(url_for('hello'))

@app.route('/transfer')
def transfer():
    bank_id = request.args.get('bank_id')
    money = request.args.get('money')
    if session.get('logged_in', None):
        if bank_id and money:
            return f'You transfer {money}$ to bank id:{bank_id}'
        return 'bank id and money amount required'
    return 'Not Authenticated'

而屠龙宝刀网页中的按钮,直接请求了transfer,向攻击者的账户转钱,而由于你的登陆信息还在,服务器以为这个请求是你发出的(实际上的确是,但并非你主观上想发出)。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <button><a href="http://127.0.0.1:8081/transfer?bank_id=4003&money=10000">屠龙宝刀,点击就送</a></button>
</body>
</html>

可是你仔细一想,这个敏感数据的修改,不应该用GET啊,这有问题。

那么,让我们把时间再次回到三天前,这次的转账页面用的是表单,POST的那种。 与此同时,一个网页吸引你的注意,硕大的“屠龙宝刀,点击就送”使你情不自禁地点击。还没来得及点击,你发现跳出的居然是转账页面。定睛一看,转入的居然还不是你的账号,金额是一万。你两眼发昏,陷入沉思。

明明使用的就是POST表单啊,为什么还是会出现问题 ?

我们不妨假设现在的转账函数变为这样:

@app.route('/transfer', methods=['POST', 'GET'])
def withdraw():
    if session.get('logged_in', None):
        bank_id = request.form.get('bank_id')
        money = request.form.get('money')
        if bank_id and money:
            return f'You transfer {money}$ to bank id:{bank_id}'
    return render_template('index.html')

显而易见,我可以针对你的转账表单构造一个页面,使其在加载过程中提交一个POST请求,由于你的登录信息仍存储在cookie里面, 服务器以为这个请求是你发出的 ,最终达到邪恶的目的。

构建的页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script type="text/javascript">
        function steal() {
            form = document.getElementById('postForm');
            form.submit()
        }
    </script>
</head>
<body onload="steal()">
    <button>屠龙宝刀,点击就送</button>
    <form method="POST" style="display: none" id="postForm" action="http://127.0.0.1:8081/transfer">
        <input type="hidden" name="bank_id" value="4003">
        <input type="hidden" name="money" value="10000">
    </form>
</body>
</html>

如何预防

1. 尽量使用POST,限制GET

GET接口太容易被拿来做CSRF攻击,看第一个示例就知道,只要构造一个img标签,而img标签又是不能过滤的数据。接口最好限制为POST使用,GET则无效,降低攻击风险。

当然POST并不是万无一失,攻击者只要构造一个form就可以,但需要在第三方页面做,这样就增加暴露的可能性。

2. 浏览器Cookie策略

IE6、7、8、Safari会默认拦截第三方本地Cookie(Third-party Cookie)的发送。但是Firefox2、3、Opera、Chrome、Android等不会拦截,所以通过浏览器Cookie策略来防御CSRF攻击不靠谱,只能说是降低了风险。

PS:Cookie分为两种,Session Cookie(在浏览器关闭后,就会失效,保存到内存里),Third-party Cookie(即只有到了Exprie时间后才会失效的Cookie,这种Cookie会保存到本地)。

PS:另外如果网站返回HTTP头包含P3P Header,那么将允许浏览器发送第三方Cookie。

3. 加验证码

验证码,强制用户必须与应用进行交互,才能完成最终请求。在通常情况下,验证码能很好遏制CSRF攻击。但是出于用户体验考虑,网站不能给所有的操作都加上验证码。因此验证码只能作为一种辅助手段,不能作为主要解决方案。

4. Referer Check

Referer Check在Web最常见的应用就是“防止图片盗链”。同理,Referer Check也可以被用于检查请求是否来自合法的“源”(Referer值是否是指定页面,或者网站的域),如果都不是,那么就极可能是CSRF攻击。

但是因为服务器并不是什么时候都能取到Referer,所以也无法作为CSRF防御的主要手段。但是用Referer Check来监控CSRF攻击的发生,倒是一种可行的方法。

5. Anti CSRF Token

现在业界对CSRF的防御,一致的做法是使用一个Token(Anti CSRF Token)。

例子:

1. 用户访问某个表单页面。

2. 服务端生成一个Token,放在用户的Session中,或者浏览器的Cookie中。

3. 在页面表单附带上Token参数。

4. 用户提交请求后, 服务端验证表单中的Token是否与用户Session(或Cookies)中的Token一致,一致为合法请求,不是则非法请求。

这个Token的值必须是随机的,不可预测的。由于Token的存在,攻击者无法再构造一个带有合法Token的请求实施CSRF攻击。另外使用Token时应注意Token的保密性,尽量把敏感操作由GET改为POST,以form或AJAX形式提交,避免Token泄露。

注意:

CSRF的Token仅仅用于对抗CSRF攻击。当网站同时存在XSS漏洞时候,那这个方案也是空谈。所以XSS带来的问题,应该使用XSS的防御方案予以解决。

预防手段摘抄自 hyddd 的博客园