阐述 JavaScript 中的 CSRF (跨站请求伪造) 攻击原理,以及如何通过 CSRF Token 或 SameSite Cookie 策略进行防御。

各位老铁,早上好!今天咱们来聊聊一个前端安全领域的老朋友,但又不得不防的家伙——CSRF (Cross-Site Request Forgery),也就是跨站请求伪造。这玩意儿听起来挺高大上,但本质上就是个“冒名顶替”的坏蛋。

1. CSRF 攻击:啥时候你被“冒名顶替”了?

想象一下,你每天登录你的银行网站,输入密码,查看余额,转账。一切正常。突然有一天,你在浏览一个看似无害的论坛,这个论坛里藏着一个精心设计的“陷阱”。当你点击了这个“陷阱”后,你的银行账户里的钱,嗖的一下,就转到了别人的账户里。

是不是觉得有点恐怖?这就是 CSRF 攻击的威力。

具体是怎么发生的呢?

  1. 信任关系建立: 你已经登录了银行网站 ( bank.example.com ),浏览器里保存了你的登录信息 (Cookie)。
  2. 攻击者埋下陷阱: 攻击者在一个恶意网站 ( 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>
  3. 受害者中招: 你在浏览这个恶意网站的时候,这个隐藏的表单自动提交了。因为你之前已经登录了银行网站,所以浏览器会自动带上银行网站的 Cookie。
  4. 银行被骗: 银行网站收到这个请求,发现 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 的核心思想是:让攻击者无法伪造完整的请求。

实现步骤:

  1. 服务器端生成 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'])
  2. 客户端携带 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 }}">
  3. 服务器端验证 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=TrueSameSite=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 攻击和防御的分享就到这里。希望大家以后都能写出安全可靠的代码,远离安全漏洞! 感谢各位老铁!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注