跨站请求伪造(CSRF,Cross-Site Request Forgery)是一种网络攻击方式,它允许攻击者在用户不知情的情况下,以用户的名义执行非授权的命令。这种攻击利用了用户已经验证的身份和信任关系,来执行恶意操作。
一、工作原理
- 用户登录:用户登录到受信任的网站A,并在浏览器中保留了登录状态(例如,通过Cookies)。
- 诱导点击:攻击者诱使该用户访问恶意网站B,网站B包含了一个向网站A发起请求的操作(如表单提交)。
- 自动请求:由于用户已经在网站A中登录,浏览器会自动附带用户在网站A的认证信息(如Cookies)发起请求。如果网站A没有适当的防护措施,这个请求就会被视为用户自愿发起的,从而执行攻击者预设的操作。
二、防御措施
2.1 使用CSRF令牌
在表单或者AJAX请求中使用一个随机生成的令牌(CSRF Token),服务器验证请求中的令牌是否有效。由于攻击者无法获取这个令牌,因此无法构造有效的请求。
使用CSRF令牌(CSRF Token)是防御跨站请求伪造(CSRF)攻击的一种常用方法。以下是一个简单的示例,展示了如何在Web应用中实现CSRF保护机制。
2.1.1 假设场景
假设我们有一个简单的表单,用户可以通过它来更新自己的个人信息。我们将使用CSRF令牌来确保只有真正的用户可以提交这个表单。
2.1.2 后端实现(以Python Flask为例)
首先,我们需要在后端生成一个CSRF令牌,并将其发送到前端页面。
from flask import Flask, render_template, request, session
import os
app = Flask(__name__)
app.secret_key = os.urandom(24) 用于安全地签名session
@app.route('/profile', methods=['GET'])
def profile():
生成CSRF令牌
csrf_token = os.urandom(16).hex()
将CSRF令牌存储在session中
session['csrf_token'] = csrf_token
将CSRF令牌发送到前端
return render_template('profile.html', csrf_token=csrf_token)
@app.route('/update-profile', methods=['POST'])
def update_profile():
从表单和session中获取CSRF令牌
form_csrf_token = request.form.get('csrf_token')
session_csrf_token = session.get('csrf_token')
验证CSRF令牌
if not form_csrf_token or form_csrf_token != session_csrf_token:
return "CSRF token mismatch", 403
处理更新逻辑...
return "Profile updated successfully"
2.1.3 前端实现
在前端页面(profile.html
),我们需要确保表单包含CSRF令牌作为一个隐藏字段。
<form action="/update-profile" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit">Update Profilebutton>
form>
2.1.4 工作流程
- 当用户访问
/profile
页面时,后端生成一个CSRF令牌,并将其存储在用户的session中,同时发送到前端页面。 - 用户填写表单并提交时,表单中包含的CSRF令牌也会被发送到后端。
- 后端接收到表单提交的请求后,会从请求中提取CSRF令牌,并将其与session中存储的令牌进行比较。
- 如果两个令牌匹配,请求被视为合法,后端继续处理表单提交的数据;如果不匹配,请求被拒绝,以防止CSRF攻击。
2.1.5 注意
- CSRF令牌应该是随机生成的,对每个用户和会话都是唯一的。
- 在用户完成表单提交后,应该重新生成CSRF令牌,以防止令牌泄露。
- 对于敏感操作(如密码更改、个人信息更新等),使用CSRF令牌是一种有效的安全措施。
2.2 检查Referer头
服务器可以验证HTTP请求头中的Referer字段,以确保请求是从受信任的域名发起的。但这种方法并不完全可靠,因为Referer头可以被禁用或伪造。
检查Referer
头是一种简单的方法,用于防御跨站请求伪造(CSRF)攻击。Referer
头部分包含了发起HTTP请求的页面的地址。通过验证这个地址,服务器可以判断请求是否来自于一个可信的源。下面是一个使用Python Flask框架实现Referer
头检查的示例:
2.2.1 Python Flask 示例
假设我们有一个敏感操作,比如用户信息的更新,我们希望确保这个操作只能从我们的网站内部发起。
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/update-info', methods=['POST'])
def update_info():
获取请求的Referer头
referer_header = request.headers.get('Referer')
检查Referer头是否存在,且是否来自我们的网站
if not referer_header or not referer_header.startswith('https://www.ourwebsite.com'):
abort(403) 如果不是,返回403禁止访问
处理更新信息的逻辑...
return "Information updated successfully"
在这个示例中,当/update-info
路由接收到POST请求时,它首先检查Referer
头。如果这个头部不存在,或者它的值不是以我们网站的域名https://www.ourwebsite.com
开始的,那么请求将被拒绝,并返回HTTP状态码403,表示禁止访问。
2.2.2 注意事项
- 安全性:虽然检查
Referer
头可以提供一定程度的安全保护,但它并不是完全可靠的。用户的浏览器可能被配置为不发送Referer
头,或者攻击者可能通过某些技术手段伪造这个头部。 - 隐私:出于隐私考虑,某些浏览器或用户可能选择禁用
Referer
头。 - 备选方案:因此,虽然检查
Referer
头可以作为CSRF防护的一部分措施,但最好与其他防御机制(如CSRF令牌)结合使用,以提供更全面的安全保护。
总的来说,检查Referer
头是一种简单的防御CSRF攻击的方法,但应该与其他安全措施结合使用,以确保Web应用的安全。
2.3 使用自定义请求头
由于跨站请求通常无法携带自定义头信息,因此检查请求中是否包含自定义头可以作为一种防御手段。
使用自定义请求头是一种防御跨站请求伪造(CSRF)攻击的技术。这种方法的基本思想是在客户端的请求中添加一个自定义的HTTP头部(Header),服务器端检查这个头部以验证请求的合法性。由于跨站请求通常无法设置自定义头部,这为服务器提供了一种简单的验证机制。
2.3.1 实现步骤
2.3.1.1 客户端(JavaScript示例)
在发送AJAX请求时,添加一个自定义的HTTP头部。例如,使用X-Requested-With
头部,这是一个常用的约定,用于标识AJAX请求。
fetch('/api/update-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 添加自定义请求头
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
username: 'newUsername'
})
}).then(response => {
if (response.ok) {
console.log('Profile updated successfully');
}
}).catch(error => {
console.error('Error updating profile:', error);
});
2.3.1.2 服务器端(Python Flask示例)
在服务器端,检查每个请求是否包含了这个自定义的头部。如果请求不包含这个头部,或者头部的值不符合预期,服务器可以拒绝这个请求。
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/api/update-profile', methods=['POST'])
def update_profile():
检查自定义请求头
if request.headers.get('X-Requested-With') != 'XMLHttpRequest':
abort(403) 如果请求头不匹配,返回403 Forbidden
处理更新逻辑...
return "Profile updated successfully"
2.3.2 注意事项
- 安全性:虽然使用自定义请求头可以提高安全性,但它不应该是唯一的防御措施。最好将其与其他安全机制(如CSRF令牌)结合使用。
- 兼容性:确保客户端能够设置自定义头部,并且服务器能够正确地处理这些头部。某些浏览器或网络环境可能会限制或修改HTTP头部。
- CORS策略:如果你的Web应用涉及跨域请求,需要确保CORS(跨源资源共享)策略正确配置,以允许自定义头部。
使用自定义请求头是一种相对简单的防御CSRF攻击的方法,它依赖于跨站请求的限制。然而,为了提供更强的安全保障,建议采用多种防御策略的组合。
2.4 双重Cookie验证
在用户登录时生成一个随机值,存储在Cookie中,并要求所有表单请求或AJAX请求都携带这个值作为参数。由于第三方网站无法读取这个Cookie,因此无法伪造请求。
双重Cookie验证是一种防御跨站请求伪造(CSRF)的技术,它利用了跨站请求无法读取或设置目标站点Cookie的特性。这种方法不需要在服务器端存储CSRF令牌的状态,因此对于一些无状态的应用场景特别有用。下面是一个具体的实现示例:
2.4.1 实现步骤
2.4.1.1 设置CSRF令牌Cookie
当用户访问你的网站并进行登录或者开始一个会话时,服务器生成一个CSRF令牌,并将这个令牌作为Cookie发送给用户的浏览器。
from flask import Flask, make_response, request, abort
import os
app = Flask(__name__)
@app.route('/login', methods=['GET', 'POST'])
def login():
登录逻辑...
生成CSRF令牌
csrf_token = os.urandom(16).hex()
创建响应对象
response = make_response("Logged in successfully.")
设置CSRF令牌Cookie
response.set_cookie('csrf_token', csrf_token)
return response
2.4.1.2 在客户端请求中包含CSRF令牌
在客户端,每次发送请求时,从Cookie中读取CSRF令牌,并将这个令牌以请求头或请求体的形式包含在请求中。
function sendRequest() {
// 从Cookie中获取CSRF令牌
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrf_token=')).split('=')[1];
fetch('/api/update-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 将CSRF令牌包含在请求头中
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
username: 'newUsername'
})
}).then(response => {
if (response.ok) {
console.log('Profile updated successfully');
}
}).catch(error => {
console.error('Error updating profile:', error);
});
}
2.4.1.3 服务器验证CSRF令牌
服务器在接收到请求时,比较请求中的CSRF令牌和Cookie中的CSRF令牌是否一致。
@app.route('/api/update-profile', methods=['POST'])
def update_profile():
从请求头和Cookie中获取CSRF令牌
request_csrf_token = request.headers.get('X-CSRF-Token')
cookie_csrf_token = request.cookies.get('csrf_token')
验证CSRF令牌
if not request_csrf_token or request_csrf_token != cookie_csrf_token:
abort(403) 如果令牌不匹配,返回403 Forbidden
处理更新逻辑...
return "Profile updated successfully"
2.4.2 注意事项
- 确保CSRF令牌足够随机,以防止攻击者猜测令牌值。
- 设置Cookie时,考虑使用
HttpOnly
和Secure
标志,以增强安全性。- HttpOnly标志用于限制Cookies只能通过HTTP(S)请求被访问,而不能通过客户端脚本(如JavaScript)直接访问。
- Secure标志用于指示Cookies只能通过安全的HTTPS连接发送给服务器。
- 双重Cookie验证方法依赖于浏览器的同源策略,但并不是所有的CSRF攻击场景都能被这种方法防御。因此,最好将其与其他安全措施(如验证Referer头或使用CSRF令牌)结合使用。
双重Cookie验证提供了一种相对简单的CSRF防护机制,特别适用于需要无状态验证的应用场景。然而,它并不是万能的,开发者应根据具体的应用需求和安全要求,选择合适的防护策略。
2.5 使用SameSite Cookie属性
设置Cookies的SameSite属性可以限制Cookies只能在同一站点的请求中发送,这有助于减少CSRF攻击的风险。
SameSite
Cookie属性是一种防止跨站请求伪造(CSRF)攻击的机制,它允许服务器指定某个Cookie不应该随着来自第三方站点的请求一起发送。SameSite
属性可以有三个值:Strict
、Lax
和None
。
Strict
:Cookie只会在浏览器直接访问原网站时发送。即使用户通过第三方网站的链接点击访问原网站,Cookie也不会被发送。Lax
:相对宽松,Cookie在跨站子请求(如图片或iframe)中不会被发送,但在用户从第三方网站导航到原网站时(例如通过链接点击),Cookie会被发送。None
:Cookie将在所有请求中被发送,但必须同时设置Secure
属性,即只能在HTTPS协议下使用。
2.5.1 如何设置SameSite属性
2.5.1.1 Python Flask示例
在Flask中设置SameSite
属性:
from flask import Flask, make_response
app = Flask(__name__)
@app.route('/set-cookie')
def set_cookie():
response = make_response("Cookie is set")
设置SameSite属性为Lax
response.set_cookie('key', 'value', samesite='Lax')
return response
2.5.1.2 Node.js Express示例
在Express中设置SameSite
属性:
const express = require('express');
const app = express();
app.get('/set-cookie', (req, res) => {
res.cookie('key', 'value', { sameSite: 'lax' });
res.send('Cookie is set');
});
2.5.1.3 PHP示例
在PHP中设置SameSite
属性:
setcookie('key', 'value', [
'expires' => time() + 3600, // 设置过期时间
'path' => '/', // 设置路径
'domain' => 'example.com', // 设置域
'secure' => true, // 启用Secure标志
'httponly' => true, // 启用HttpOnly标志
'samesite' => 'Lax', // 设置SameSite属性为Lax
]);
2.5.1 注意事项
SameSite=None
必须与Secure
一起使用,这意味着Cookie只能在HTTPS连接中发送。- 不是所有浏览器都支持
SameSite
属性,尤其是一些旧版本的浏览器。在这些浏览器中,设置SameSite
属性可能没有任何效果。 - 在实现跨域认证或跨站资源共享(CORS)时,需要仔细考虑
SameSite
属性的设置,以避免不必要的访问限制。
通过合理使用SameSite
属性,可以有效减少CSRF攻击的风险,增强Web应用的安全性。
三、结论
CSRF是一种严重的安全威胁,它利用了Web应用中的信任机制。通过实施上述防御措施,开发者可以显著降低应用遭受CSRF攻击的风险。在设计Web应用和API时,始终考虑到CSRF防护是非常重要的。