各位老铁,早上好!今天咱们来聊聊一个前端安全领域的老朋友,但又不得不防的家伙——CSRF (Cross-Site Request Forgery),也就是跨站请求伪造。这玩意儿听起来挺高大上,但本质上就是个“冒名顶替”的坏蛋。
1. CSRF 攻击:啥时候你被“冒名顶替”了?
想象一下,你每天登录你的银行网站,输入密码,查看余额,转账。一切正常。突然有一天,你在浏览一个看似无害的论坛,这个论坛里藏着一个精心设计的“陷阱”。当你点击了这个“陷阱”后,你的银行账户里的钱,嗖的一下,就转到了别人的账户里。
是不是觉得有点恐怖?这就是 CSRF 攻击的威力。
具体是怎么发生的呢?
- 信任关系建立: 你已经登录了银行网站 (
bank.example.com
),浏览器里保存了你的登录信息 (Cookie)。 - 攻击者埋下陷阱: 攻击者在一个恶意网站 (
evil.example.com
) 上放置了一个精心构造的请求,比如一个隐藏的表单,指向你的银行网站的转账接口。<!-- evil.example.com --> <form action="https://bank.example.com/transfer" method="POST"> <input type="hidden" name="account" value="attacker_account"> <input type="hidden" name="amount" value="1000"> <input type="submit" value="领取免费礼品"> </form> <script> document.forms[0].submit(); // 自动提交表单 </script>
- 受害者中招: 你在浏览这个恶意网站的时候,这个隐藏的表单自动提交了。因为你之前已经登录了银行网站,所以浏览器会自动带上银行网站的 Cookie。
- 银行被骗: 银行网站收到这个请求,发现 Cookie 是你的,就认为是你发起的转账请求,于是就把钱转走了。
关键点:
- 信任 Cookie: 银行网站依赖 Cookie 来识别用户身份。
- 浏览器自动携带 Cookie: 浏览器会自动把与请求域名匹配的 Cookie 发送出去。
- 攻击者伪造请求: 攻击者利用你的身份,伪造了一个请求,欺骗了银行网站。
用一句话概括:CSRF 攻击就是利用用户的登录状态,在用户不知情的情况下,以用户的名义发送恶意请求。
2. CSRF 攻击的类型
CSRF 攻击主要有两种类型:
-
GET 型 CSRF: 攻击者通过构造 URL 来发起请求。
<img src="https://bank.example.com/transfer?account=attacker_account&amount=1000">
这种攻击方式比较简单,只需要一个
<img>
标签就可以搞定。 -
POST 型 CSRF: 攻击者通过构造表单来发起请求。
<form action="https://bank.example.com/transfer" method="POST"> <input type="hidden" name="account" value="attacker_account"> <input type="hidden" name="amount" value="1000"> </form> <script> document.forms[0].submit(); </script>
这种攻击方式稍微复杂一些,需要一个
<form>
标签和一个 JavaScript 脚本。
3. 如何防御 CSRF 攻击?
既然 CSRF 攻击这么可怕,那我们该如何防御呢?主要有两种方法:
- CSRF Token: 在每个请求中添加一个随机的、不可预测的 Token,服务器端验证这个 Token 的合法性。
- SameSite Cookie: 通过设置 Cookie 的
SameSite
属性,限制 Cookie 的跨域使用。
接下来,我们分别详细讲解这两种方法。
3.1 CSRF Token 防御
CSRF Token 的核心思想是:让攻击者无法伪造完整的请求。
实现步骤:
-
服务器端生成 Token: 当用户访问需要保护的页面时,服务器端生成一个随机的、不可预测的 Token,并将其保存在 Session 中。
# Python (Flask) 示例 import os from flask import Flask, session, render_template, request, redirect, url_for app = Flask(__name__) app.secret_key = os.urandom(24) # 设置一个安全的 secret key @app.route('/protected') def protected(): session['csrf_token'] = os.urandom(16).hex() # 生成随机 CSRF token return render_template('protected.html', csrf_token=session['csrf_token'])
-
客户端携带 Token: 将 Token 嵌入到 HTML 表单中,或者通过 JavaScript 将 Token 添加到请求头中。
<!-- protected.html --> <form action="/transfer" method="POST"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="text" name="account" placeholder="Account"> <input type="text" name="amount" placeholder="Amount"> <button type="submit">Transfer</button> </form>
或者,使用 JavaScript (例如,使用
fetch
API):// JavaScript 示例 fetch('/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content // 从meta标签获取token }, body: JSON.stringify({ account: 'attacker_account', amount: 1000 }) });
需要在HTML中添加meta标签:
<meta name="csrf-token" content="{{ csrf_token }}">
-
服务器端验证 Token: 当服务器端收到请求时,验证请求中携带的 Token 是否与 Session 中保存的 Token 一致。如果一致,则认为请求是合法的;否则,拒绝请求。
# Python (Flask) 示例 @app.route('/transfer', methods=['POST']) def transfer(): csrf_token = request.form.get('csrf_token') if csrf_token != session.get('csrf_token'): return "CSRF 攻击!", 403 account = request.form.get('account') amount = request.form.get('amount') # 执行转账操作 return f"Transfered {amount} to {account}"
对于使用
fetch
API 和X-CSRF-Token
的情况:# Python (Flask) 示例 @app.route('/transfer', methods=['POST']) def transfer(): csrf_token = request.headers.get('X-CSRF-Token') if csrf_token != session.get('csrf_token'): return "CSRF 攻击!", 403 data = request.get_json() account = data.get('account') amount = data.get('amount') # 执行转账操作 return f"Transfered {amount} to {account}"
代码示例总结 (简易版):
步骤 | 客户端 (HTML/JavaScript) | 服务器端 (Python/Flask) |
---|---|---|
生成 Token | python session['csrf_token'] = os.urandom(16).hex() |
|
携带 Token | html <input type="hidden" name="csrf_token" value="{{ csrf_token }}"> 或者 JavaScript: javascript headers: { 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content } (需要先在HTML中用meta标签存放token) |
|
验证 Token | python csrf_token = request.form.get('csrf_token') if csrf_token != session.get('csrf_token'): return "CSRF 攻击!", 403 (或者从header中获取) |
注意事项:
- Token 的随机性: Token 必须是随机的、不可预测的,否则攻击者可以通过某种方式猜测出 Token 的值。
- Token 的唯一性: 每个用户的 Token 应该是不一样的。
- Token 的时效性: Token 应该有一定的有效期,过期后需要重新生成。
- Token 的保密性: Token 应该保存在 Session 中,避免泄露给攻击者。
- 所有关键操作都要验证 Token: 不仅仅是 POST 请求,PUT、DELETE 等等修改数据的请求都需要验证 Token。
优点:
- 防御效果好,可以有效防止 CSRF 攻击。
缺点:
- 实现起来稍微复杂一些,需要在服务器端和客户端都进行修改。
- 需要维护 Session,会增加服务器的负担。
3.2 SameSite Cookie 防御
SameSite
是 Cookie 的一个属性,用于限制 Cookie 的跨域使用。通过设置 SameSite
属性,可以告诉浏览器,只有在同源的情况下才能发送 Cookie。
SameSite
属性有三个值:
Strict
: 只有在同源的情况下才能发送 Cookie。这意味着,即使是同一个网站内部的跨页面跳转,如果使用了不同的协议 (例如,从 HTTPS 跳转到 HTTP),也不会发送 Cookie。Lax
: 在大多数情况下,只有在同源的情况下才能发送 Cookie。但是,当用户从外部网站通过链接 (例如,<a>
标签) 访问当前网站时,也会发送 Cookie。None
: 允许跨域发送 Cookie。但是,如果设置了SameSite=None
,必须同时设置Secure
属性,表示 Cookie 只能在 HTTPS 连接下发送。
如何设置 SameSite
属性?
在服务器端设置 Cookie 时,可以设置 SameSite
属性:
# Python (Flask) 示例
from flask import Flask, make_response
app = Flask(__name__)
@app.route('/')
def index():
resp = make_response("Hello, World!")
resp.set_cookie('session_id', '123456', samesite='Strict', secure=True) # 设置 SameSite 属性
return resp
或者,在 JavaScript 中设置 Cookie 时:
// JavaScript 示例
document.cookie = "session_id=123456; SameSite=Strict; Secure";
代码示例总结 (简易版):
设置方式 | 代码 |
---|---|
服务器端 (Python) | python resp.set_cookie('session_id', '123456', samesite='Strict', secure=True) (注意 secure=True 在 SameSite=None 时必须设置) |
客户端 (JavaScript) | javascript document.cookie = "session_id=123456; SameSite=Strict; Secure"; (同样注意 Secure 属性) |
选择哪个值?
Strict
: 如果你的网站不需要跨域访问,或者只需要非常有限的跨域访问,那么Strict
是一个很好的选择。它可以提供最强的 CSRF 防御能力。Lax
: 如果你的网站需要允许用户从外部网站通过链接访问,那么可以选择Lax
。None
: 只有在你的网站需要跨域访问,并且你已经采取了其他的安全措施 (例如,CORS) 的情况下,才能选择None
。并且必须同时设置Secure
属性。
注意事项:
- 浏览器兼容性:
SameSite
属性并不是所有浏览器都支持,需要考虑兼容性问题。 可以查询Can I use网站的浏览器支持情况。 Secure
属性: 如果设置了SameSite=None
,必须同时设置Secure
属性,表示 Cookie 只能在 HTTPS 连接下发送。
优点:
- 实现起来比较简单,只需要在服务器端设置 Cookie 属性即可。
- 可以有效防止 CSRF 攻击。
缺点:
- 浏览器兼容性问题。
- 可能会影响某些跨域场景下的用户体验。
4. 总结
CSRF 攻击是一种常见的 Web 安全漏洞,攻击者可以利用用户的登录状态,在用户不知情的情况下,以用户的名义发送恶意请求。
防御 CSRF 攻击的主要方法有两种:
- CSRF Token: 在每个请求中添加一个随机的、不可预测的 Token,服务器端验证这个 Token 的合法性。
- SameSite Cookie: 通过设置 Cookie 的
SameSite
属性,限制 Cookie 的跨域使用。
选择哪种方法取决于你的具体需求和场景。一般来说,CSRF Token
的防御效果更好,但实现起来稍微复杂一些。SameSite Cookie
的实现比较简单,但可能会影响某些跨域场景下的用户体验。
最终建议:
- 优先使用
CSRF Token
。 虽然实现复杂一点,但是安全效果更好。 - 如果条件允许,同时使用
SameSite Cookie
。 可以作为额外的防御层。 - 对于重要的操作,一定要进行二次验证。 例如,让用户输入密码或者验证码。
- 保持警惕,定期进行安全审查。
好了,今天关于 CSRF 攻击和防御的分享就到这里。希望大家以后都能写出安全可靠的代码,远离安全漏洞! 感谢各位老铁!