🚀 大家好,我是小康。

今天给大家分享一个 网络安全面试题 :什么是 CSRF 攻击?如何避免?

小技巧:在面试中,可以参考下面的示例回答,这样回答简洁明了。详细介绍部分则是为了帮助大家系统学习,以便应对面试官深入提问。

示例回答

CSRF攻击(跨站请求伪造)是一种通过欺骗用户在已登录状态下执行非预期操作的攻击。为了避免CSRF攻击,我们可以使用CSRF令牌、SameSite Cookie属性以及用户确认步骤。


详细解释

什么是 CSRF 攻击?
CSRF(Cross-Site Request Forgery,跨站请求伪造)攻击是一种利用已认证用户的身份,向受信任的网站发送恶意请求的攻击方式。攻击者通过诱导用户点击恶意链接、访问恶意网站或嵌入恶意脚本等手段,在用户不知情的情况下,伪造合法请求发送给受信任的网站,从而执行非预期的操作,例如转账、修改用户信息等。
CSRF 攻击的工作原理

  1. 用户登录受信任网站
  • 用户在浏览器中登录了受信任的网站,并保持登录状态(例如,浏览器中保存了会话Cookie)。
  1. 攻击者准备恶意请求
  • 攻击者构造一个恶意请求,例如转账请求或账户设置修改请求。
  1. 用户访问恶意网站
  • 用户在保持登录状态下,访问了攻击者控制的恶意网站,或点击了攻击者发送的恶意链接。
  1. 恶意请求被执行
  • 恶意网站在用户不知情的情况下,自动向受信任的网站发送伪造请求。由于用户已登录,受信任的网站会将请求视为合法用户的请求并执行相应操作(例如转账请求)。

示例

假设你在一个银行网站上登录,URL为https://bank.com,并保持登录状态。攻击者发送了一封电子邮件,诱导你点击一个恶意链接:

1
2
3
html
复制代码
<a href="https://bank.com/transfer?amount=1000&to_account=attacker">Click here to see something interesting</a>

当你点击这个链接,浏览器会向银行网站发送转账请求,由于你已登录,银行网站会执行这次转账操作。

如何避免 CSRF 攻击?

1. 使用动态的CSRF令牌

原理:在每个敏感操作的表单中嵌入一个唯一的、随机生成的CSRF令牌,每次请求都会生成新的令牌。服务器端在接收到请求时,验证该令牌是否正确,以确保请求是合法的。
示例

  • 生成和嵌入CSRF令牌: 当用户访问页面时,服务器生成一个唯一的CSRF令牌并存储在会话中,然后将该令牌嵌入到表单的隐藏字段中。
  • 验证CSRF令牌: 当用户提交表单时,服务器从请求中获取CSRF令牌,并与会话中的令牌进行比较。如果匹配,则处理请求并生成新的CSRF令牌。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    # 示例代码(Flask)
    @app.route('/')
    def index():
    csrf_token = secrets.token_urlsafe()
    session['csrf_token'] = csrf_token
    return render_template_string('''
    <form method="post" action="/transfer">
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
    <input type="text" name="amount" placeholder="Amount">
    <input type="text" name="to_account" placeholder="Recipient">
    <input type="submit" value="Submit">
    </form>
    ''', csrf_token=csrf_token)

    @app.route('/transfer', methods=['POST'])
    def transfer():
    token = request.form.get('csrf_token')
    stored_token = session.pop('csrf_token', None) # 从会话中移除令牌
    if not token or token != stored_token:
    abort(403) # Forbidden
    # 生成新的CSRF令牌
    new_csrf_token = secrets.token_urlsafe()
    session['csrf_token'] = new_csrf_token
    return f'Transfer completed successfully, new CSRF token: {new_csrf_token}'

2. 使用自定义HTTP头部

原理:在前端请求时,添加自定义的HTTP头部,例如X-CSRF-Token,服务器在接收到请求时,验证该头部的值是否正确。每次请求都使用新的CSRF令牌。
示例

  • 前端发送请求时添加自定义HTTP头部: 前端在发起请求时,将CSRF令牌添加到自定义HTTP头部中。
  • 服务器验证自定义HTTP头部: 服务器从请求头中获取CSRF令牌,并与会话中的令牌进行比较。如果匹配,则处理请求并生成新的CSRF令牌。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 示例代码(前端)
    function sendRequest() {
    const xhr = new XMLHttpRequest();
    xhr.open("POST", "/transfer", true);
    xhr.setRequestHeader("X-CSRF-Token", "{{ csrf_token }}");
    xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
    }
    };
    xhr.send();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    # 示例代码(Flask 后端代码)

    # 代码功能:在用户访问页面时生成一个唯一的CSRF令牌,并将其存储在会话中和嵌入到前端JavaScript代码中。
    # 当用户点击按钮时,前端代码会通过XMLHttpRequest发送一个带有CSRF令牌的POST请求到服务器。

    @app.route('/')
    def index():
    csrf_token = secrets.token_urlsafe()
    session['csrf_token'] = csrf_token
    return render_template_string('''
    <button onclick="sendRequest()">Submit</button>
    <script>
    function sendRequest() {
    const xhr = new XMLHttpRequest();
    xhr.open("POST", "/transfer", true);
    xhr.setRequestHeader("X-CSRF-Token", "{{ csrf_token }}");
    xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
    }
    };
    xhr.send();
    }
    </script>
    ''', csrf_token=csrf_token)


    # 这个代码在处理POST请求时,从请求头中获取CSRF令牌并与会话中的令牌进行比较。
    # 如果令牌匹配,则处理请求并生成一个新的CSRF令牌存储在会话中,同时返回成功信息和新的CSRF令牌。
    # 这样可以确保请求的合法性并防止CSRF攻击。

    @app.route('/transfer', methods=['POST'])
    def transfer():
    token = request.headers.get('X-CSRF-Token')
    stored_token = session.pop('csrf_token', None) # 从会话中移除令牌
    if not token or token != stored_token:
    abort(403) # Forbidden
    # 生成新的CSRF令牌
    new_csrf_token = secrets.token_urlsafe()
    session['csrf_token'] = new_csrf_token
    return f'Transfer completed successfully, new CSRF token: {new_csrf_token}'

这里说下使用动态的CSRF令牌和自定义HTTP头部的区别
使用动态的CSRF令牌主要适用于传统的表单提交,它通过在表单中嵌入一个每次访问页面时都会更新的令牌来防护。而自定义HTTP头部,特别是用于AJAX和API请求,通过在每次请求中添加一个可能会更新的令牌到HTTP请求头中,使得它更适合现代的Web应用程序。简单来说,前者多用于用户直接交互的表单,后者则适用于后台数据交互。

3. 使用SameSite Cookie属性

原理
SameSite Cookie属性通过限制Cookie的发送范围来防止浏览器在跨站请求中发送Cookie,从而避免CSRF攻击。具体来说,SameSite属性有三个值:

  • Strict:Cookie仅在同站请求中发送,即只有在请求源与Cookie所在的站点完全相同时,Cookie才会被发送。这是最严格的设置,防止所有跨站请求发送Cookie。
  • Lax:Cookie在同站请求中发送,以及用户导航到目标站点的跨站请求中发送(例如,通过点击链接)。这种模式在防止CSRF攻击的同时,允许大多数正常的跨站功能。
  • None:Cookie在所有请求中发送,包括跨站请求。这种模式不提供防止CSRF攻击的保护。

如何工作?
当Cookie设置了SameSite属性为StrictLax时,浏览器会限制跨站请求中的Cookie发送:

  1. SameSite=Strict:如果一个请求是从不同的站点发起的(即跨站请求),浏览器不会发送这个Cookie。因此,攻击者无法利用用户的Cookie进行伪造请求。
  2. SameSite=Lax:浏览器只会在用户导航操作(如点击链接)发起的跨站请求中发送Cookie,而不会在其他类型的跨站请求(如表单提交、图片加载等)中发送Cookie,从而减少CSRF攻击的风险。

实际例子
假设用户登录了一个银行网站,该网站的Cookie设置了SameSite=Lax属性:

1
Set-Cookie: sessionid=abc123; SameSite=Lax

场景1:正常操作

  • 用户在银行网站上进行操作,如查看账户信息或转账。
  • 因为这些请求都是从同一个站点发起的,浏览器会发送Cookie(sessionid=abc123),服务器会验证用户身份并处理请求。

场景2:CSRF攻击尝试

  • 攻击者在恶意网站上放置了一个表单,诱骗用户提交该表单向银行网站发起转账请求。

攻击者网站上的恶意表单:

1
2
3
4
5
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="amount" value="1000">
<input type="hidden" name="to_account" value="attacker_account">
<input type="submit" value="Transfer Money">
</form>
  • 用户在不知情的情况下点击了提交按钮,表单向银行网站发送了请求。
  • 由于请求是从不同的站点发起的(跨站请求),浏览器不会发送带有SameSite=Lax属性的Cookie(sessionid=abc123)。这样,攻击者就无法利用用户的Cookie进行伪造请求。

结果

  • 银行网站接收到请求,但因为没有sessionid Cookie,无法验证用户的身份,服务器会拒绝处理请求。
  • 这样,攻击者就无法利用用户的登录状态在银行网站上进行伪造的转账操作,CSRF攻击被阻止。

通过设置SameSite Cookie属性,浏览器限制了在跨站请求中发送Cookie的行为,有效防止了CSRF攻击。这个方法简单而有效,适用于大多数Web应用的CSRF防护。

4、使用确保重要操作需要用户确认

原理:确保重要操作需要用户确认是一种有效的防止CSRF攻击的方法。通过增加二次确认步骤,即使攻击者设法让用户发起了一个请求,也无法完成关键操作,因为需要额外的用户验证。这种方式增加了额外的安全检查,使得攻击者无法绕过这些验证步骤。
如何工作?
当用户进行敏感操作时(如转账、修改密码等),系统要求用户重新输入密码、提供验证码或通过其他方式进行确认,以确保操作是由用户本人发起的,而不是被恶意网站伪造的。
实际例子
假设用户登录了一个银行网站,并要进行转账操作。为了防止CSRF攻击,银行网站在用户提交转账请求时,要求用户输入密码进行二次确认。
场景1:正常操作

  1. 用户操作
  • 用户在银行网站上选择转账,并填写转账金额和接收账户信息。
  1. 确认操作
  • 在用户提交转账请求时,系统弹出一个对话框,要求用户输入当前密码进行确认。
  1. 验证操作
  • 用户输入密码,系统验证密码是否正确。如果正确,完成转账操作。

场景2:CSRF攻击尝试

  1. 攻击者设置陷阱
  • 攻击者在恶意网站上放置了一个表单,诱骗用户提交该表单向银行网站发起转账请求。

攻击者网站上的恶意表单:

1
2
3
4
5
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="amount" value="1000">
<input type="hidden" name="to_account" value="attacker_account">
<input type="submit" value="Transfer Money">
</form>
  1. 用户无意点击
  • 用户在不知情的情况下点击了提交按钮,表单向银行网站发送了请求。
  1. 系统要求确认
  • 银行网站接收到转账请求,但在处理之前,系统弹出一个对话框,要求用户输入当前密码进行确认。
  1. 验证失败
  • 因为这是一个伪造请求,用户无法输入正确的密码进行确认。
  • 系统拒绝处理转账请求。

实际代码示例

1
2
3
4
5
6
7
<!-- 用户提交转账表单 -->
<form method="post" action="/transfer">
<input type="text" name="amount" placeholder="Amount">
<input type="text" name="to_account" placeholder="Recipient">
<input type="password" name="password" placeholder="Enter your password to confirm">
<input type="submit" value="Submit">
</form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 示例代码(Flask 后端)
@app.route('/transfer', methods=['POST'])
def transfer():
amount = request.form.get('amount')
to_account = request.form.get('to_account')
password = request.form.get('password')

# 验证用户密码是否正确
if not validate_user_password(password):
abort(403) # Forbidden

# 处理转账操作
process_transfer(amount, to_account)
return 'Transfer completed successfully'

通过确保重要操作需要用户确认,可以有效防止CSRF攻击。即使攻击者成功发起了伪造请求,系统也会在处理关键操作前要求用户进行额外的验证步骤,如重新输入密码。这种方式增加了操作的安全性,使得攻击者无法绕过这些验证,从而保护用户的敏感操作不被恶意利用。
这种方法的关键在于确保每一个重要的操作都需要用户的主动确认,增加了安全层,使得CSRF攻击变得无效。

总结

关于如何有效避免CSRF攻击,这里简单总结下:

  1. 使用动态的CSRF令牌:每次请求生成一个唯一的令牌,并在服务器端进行验证,确保请求的合法性。
  2. 使用自定义HTTP头部:在请求中添加自定义头部并验证其值,防止跨站请求。
  3. 使用SameSite Cookie属性:限制Cookie的发送范围,防止浏览器在跨站请求中发送Cookie。
  4. 确保重要操作需要用户确认:对关键操作进行二次验证,如要求重新输入密码,确保操作是由用户本人发起的。

通过这些措施,可以显著提高Web应用的安全性,防止CSRF攻击,保护用户的数据和操作安全。

最后:

欢迎大家关注我的微信公众号「跟着小康学编程」!本号致力于分享C/C++/Go/Java 语言学习、计算机基础原理、Linux 编程、数据库、微服务、容器技术 等内容。文章力求通俗易懂,并配有代码示例,方便初学者理解。如果您对这些内容感兴趣,欢迎关注我的公众号「跟着小康学编程」。

后续,我还会陆续分享各个方向的编程面试题,包括C/C++、Java、Go,以及操作系统、计算机网络、数据结构、数据库和微服务等领域,为大家的面试提供帮助。

此外,小康最近创建了一个技术交流群,专门用来讨论技术问题和解答读者的疑问。在阅读文章时,如果有不理解的知识点,欢迎大家加入交流群提问。我会尽力为大家解答。期待与大家共同进步!