- CSRF全拼為Cross Site Request Forgery冬殃,譯為跨站請(qǐng)求偽造魂角。
- CSRF指攻擊者盜用了你的身份遂填,以你的名義發(fā)送惡意請(qǐng)求。
- 包括:以你名義發(fā)送郵件逼蒙,發(fā)消息从绘,盜取你的賬號(hào),甚至于購買商品是牢,虛擬貨幣轉(zhuǎn)賬......
- 造成的問題:個(gè)人隱私泄露以及財(cái)產(chǎn)安全僵井。
CSRF攻擊示意圖
-
客戶端訪問服務(wù)器時(shí)沒有同服務(wù)器做安全驗(yàn)證
防止CSRF攻擊
步驟
- 在客戶端向后端請(qǐng)求界面數(shù)據(jù)的時(shí)候,后端會(huì)往響應(yīng)中的cookie中設(shè)置csrf_token的值
- 在Form表單中添加一個(gè)隱藏的字段驳棱,值也是csrf_token
- 在用戶點(diǎn)擊提交的時(shí)候驹沿,會(huì)帶上這兩個(gè)值向后臺(tái)發(fā)起請(qǐng)求
- 后端接收到請(qǐng)求,會(huì)以此啊幾件事件:
- 從cookie中取出csrf_token的值
- 從表單數(shù)據(jù)中取出隱藏的csrf_token的值
- 進(jìn)行對(duì)比
- 如果比較之后兩個(gè)值一樣蹈胡,那么代表是正常的請(qǐng)求,如果沒有取到或者比較不一樣朋蔫,代表不是正常的請(qǐng)求罚渐,不會(huì)執(zhí)行下一步操作
代碼演示
未進(jìn)行 csrf 校驗(yàn)的 WebA
- 后端代碼實(shí)現(xiàn)
from flask import Flask, render_template, make_response
from flask import redirect
from flask import request
from flask import url_for
app = Flask(__name__)
@app.route('/', methods=["POST", "GET"])
def index():
if request.method == "POST":
# 取到表單中提交上來的參數(shù)
username = request.form.get("username")
password = request.form.get("password")
if not all([username, password]):
print('參數(shù)錯(cuò)誤')
else:
print(username, password)
if username == 'laowang' and password == '1234':
# 狀態(tài)保持,設(shè)置用戶名到cookie中表示登錄成功
response = redirect(url_for('transfer'))
response.set_cookie('username', username)
return response
else:
print('密碼錯(cuò)誤')
return render_template('temp_login.html')
@app.route('/transfer', methods=["POST", "GET"])
def transfer():
# 從cookie中取到用戶名
username = request.cookies.get('username', None)
# 如果沒有取到驯妄,代表沒有登錄
if not username:
return redirect(url_for('index'))
if request.method == "POST":
to_account = request.form.get("to_account")
money = request.form.get("money")
print('假裝執(zhí)行轉(zhuǎn)操作荷并,將當(dāng)前登錄用戶的錢轉(zhuǎn)賬到指定賬戶')
return '轉(zhuǎn)賬 %s 元到 %s 成功' % (money, to_account)
# 渲染轉(zhuǎn)換頁面
response = make_response(render_template('temp_transfer.html'))
return response
if __name__ == '__main__':
app.run(debug=True, port=9000)
- 前端登錄頁面代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登錄</title>
</head>
<body>
<h1>我是網(wǎng)站A,登錄頁面</h1>
<form method="post">
<label>用戶名:</label><input type="text" name="username" placeholder="請(qǐng)輸入用戶名"><br/>
<label>密碼:</label><input type="password" name="password" placeholder="請(qǐng)輸入密碼"><br/>
<input type="submit" value="登錄">
</form>
</body>
</html>
- 前端代碼轉(zhuǎn)賬代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>轉(zhuǎn)賬</title>
</head>
<body>
<h1>我是網(wǎng)站A青扔,轉(zhuǎn)賬頁面</h1>
<form method="post">
<label>賬戶:</label><input type="text" name="to_account" placeholder="請(qǐng)輸入要轉(zhuǎn)賬的賬戶"><br/>
<label>金額:</label><input type="number" name="money" placeholder="請(qǐng)輸入轉(zhuǎn)賬金額"><br/>
<input type="submit" value="轉(zhuǎn)賬">
</form>
</body>
</html>
攻擊網(wǎng)站B代碼
- 后端代碼實(shí)現(xiàn)
from flask import Flask
from flask import render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('temp_index.html')
if __name__ == '__main__':
app.run(debug=True, port=8000)
- 前端代碼實(shí)現(xiàn)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>我是網(wǎng)站B</h1>
<form method="post" action="http://127.0.0.1:9000/transfer">
<input type="hidden" name="to_account" value="999999">
<input type="hidden" name="money" value="190000" hidden>
<input type="submit" value="點(diǎn)擊領(lǐng)取優(yōu)惠券">
</form>
</body>
</html>
--運(yùn)行測(cè)試源织,在用戶登錄網(wǎng)站A的情況下翩伪,點(diǎn)擊網(wǎng)站B的按鈕,可以實(shí)現(xiàn)偽造訪問
在網(wǎng)站A中模擬實(shí)現(xiàn)csrf_token校驗(yàn)的流程
- 添加生成csrf_token的函數(shù)
# 生成csrf_token的函數(shù)
def generate_csrf():
return bytes.decode(base64.b64encode(os.urandom(48)))
- 在渲染轉(zhuǎn)賬頁面的時(shí)候谈息,做以下幾件事情:
- 生成csrf_token的值
- 在返回轉(zhuǎn)賬頁面的響應(yīng)里面設(shè)置csrf_token到cookie中
- 將csrf_token保存到變單的隱藏字段中
@app.route('/transfer', methods=["POST", "GET"])
def transfer():
...
# 生成 csrf_token 的值
csrf_token = generate_csrf()
# 渲染轉(zhuǎn)換頁面缘屹,傳入 csrf_token 到模板中
response = make_response(render_template('temp_transfer.html', csrf_token=csrf_token))
# 設(shè)置csrf_token到cookie中,用于提交校驗(yàn)
response.set_cookie('csrf_token', csrf_token)
return response
- 在轉(zhuǎn)賬模板表單中添加 csrf_token 隱藏字段
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>賬戶:</label><input type="text" name="to_account" placeholder="請(qǐng)輸入要轉(zhuǎn)賬的賬戶"><br/>
<label>金額:</label><input type="number" name="money" placeholder="請(qǐng)輸入轉(zhuǎn)賬金額"><br/>
<input type="submit" value="轉(zhuǎn)賬">
</form>
- 在執(zhí)行轉(zhuǎn)賬邏輯之前進(jìn)行 csrf_token 的校驗(yàn)
if request.method == "POST":
to_account = request.form.get("to_account")
money = request.form.get("money")
# 取出表單中的 csrf_token
form_csrf_token = request.form.get("csrf_token")
# 取出 cookie 中的 csrf_token
cookie_csrf_token = request.cookies.get("csrf_token")
# 進(jìn)行對(duì)比
if cookie_csrf_token != form_csrf_token:
return 'token校驗(yàn)失敗侠仇,可能是非法操作'
print('假裝執(zhí)行轉(zhuǎn)操作轻姿,將當(dāng)前登錄用戶的錢轉(zhuǎn)賬到指定賬戶')
return '轉(zhuǎn)賬 %s 元到 %s 成功' % (money, to_account)
- 運(yùn)行測(cè)試,用戶直接在網(wǎng)站A操作沒有問題逻炊,在去網(wǎng)站B進(jìn)行操作互亮,發(fā)現(xiàn)轉(zhuǎn)賬不成功,因?yàn)榫W(wǎng)站B獲取不到表單中的csrf_token的隱藏字段余素,而且瀏覽器有 同源策略豹休,網(wǎng)站B是獲取不到網(wǎng)站A的cookie的,所以就解決了 跨站請(qǐng)求偽造 的問題
在 Flask 項(xiàng)目中解決 CSRF 攻擊
CSRF
- 包含請(qǐng)求體的請(qǐng)求都需要開啟CSRF
# CSRF1. 在app創(chuàng)建的地方開啟CSRFProtect保護(hù)
# 開啟保護(hù)之后,程序會(huì)獲取cookie中的隨機(jī)值,以及從表單或者ajax中獲取隨機(jī)值,進(jìn)行對(duì)比
# if 對(duì)比失敗,則無法訪問路由
# 后續(xù)需要設(shè)置隨機(jī)值到cookie中,以及增加ajax的headers
from flask_wtf.csrf import CSRFProtect
...
app.config.from_object(Config)
...
CSRFProtect(app)
- CSRFProtect只做校驗(yàn)工作桨吊,cookie中的csrf_token和表單中的csrf_token需要我們自己實(shí)現(xiàn)
思路分析
一威根、在 前端發(fā)起的POST請(qǐng)求 中沒有進(jìn)行csrf_token校驗(yàn),根據(jù)csrf_token校驗(yàn)原理屏积,具體操作步驟有以下幾步:
- 后端生成csrf_token的值医窿,在前端請(qǐng)求界面的時(shí)候?qū)⒅祩鹘o前端,傳給前端的方式可能有以下兩種方式:
- 在模版中的From表單中的添加隱藏的字段
- 將csrf_token使用cookie的方式傳給前端
- 在前端 發(fā)起請(qǐng)求時(shí)炊林,在表單或者請(qǐng)求頭中帶上指定的csrf_token
- 后端在接收請(qǐng)求之后姥卢,取到前端發(fā)送過來的csrf_token,與第一步生成的csrf_token值進(jìn)行校驗(yàn)
- 如果校驗(yàn)對(duì)csrf_token一致渣聚,則代表是正常的請(qǐng)求独榴,否則是偽造的請(qǐng)求,不予通過
二奕枝、在Flask中棺榔,CSRFProtect這個(gè)類專門只對(duì)指定的app進(jìn)行csrf_token校驗(yàn)操作,所以開發(fā)者需要做以下的操作:
- 生成csrf_token的值
- 將csrf_token的值傳給前端瀏覽器
完成代碼邏輯
- 生成csrf_token的值
# CSRF2. 增加請(qǐng)求鉤子,在請(qǐng)求之后設(shè)置cookie
# 我們無法判斷用戶第一次訪問網(wǎng)頁隘道,是哪個(gè)網(wǎng)頁症歇,不能寫死給那個(gè)路由的某個(gè)我網(wǎng)頁
# 所有就需要對(duì)所有飛請(qǐng)求進(jìn)行監(jiān)聽
# 導(dǎo)入生成 csrf_token 值的函數(shù)
from flask_wtf.csrf import generate_csrf
# 調(diào)用函數(shù)生成 csrf_token
csrf_token = generate_csrf()
- 將 csrf_token 的值傳給前端瀏覽器
- 實(shí)現(xiàn)思路:可以在請(qǐng)求勾子函數(shù)中完成此邏輯
# CSRF3. generate_csrf()-->源碼-->csrf_token會(huì)被緩存起來,多次調(diào)用,只會(huì)返回相同的token(被強(qiáng)制刪除或者cookie過期,會(huì)生成新的token)
# 如果獲取csrf_token,可以使用session['csrf_token']--->token會(huì)被自動(dòng)擴(kuò)展到session中
# 應(yīng)該對(duì)所有的post/put/delete/請(qǐng)求增加ajax的headers或者表單的隱藏字段(對(duì)數(shù)據(jù)有修改/提交/設(shè)置)
@app.after_request
def after_request(response):
# 調(diào)用函數(shù)生成 csrf_token
csrf_token = generate_csrf()
# 通過 cookie 將值傳給前端
response.set_cookie("csrf_token", csrf_token)
return response
- 在前端請(qǐng)求時(shí)帶上 csrf_token 值
- 根據(jù)登錄和注冊(cè)的業(yè)務(wù)邏輯谭梗,當(dāng)前采用的是 ajax 請(qǐng)求
- 所以在提交登錄或者注冊(cè)請(qǐng)求時(shí)忘晤,需要在請(qǐng)求頭中添加 X-CSRFToken 的鍵值對(duì)
$.ajax({
url:"/passport/register",
type: "post",
headers: {
"X-CSRFToken": getCookie("csrf_token")
},
data: JSON.stringify(params),
contentType: "application/json",
success: function (resp) {
if (resp.errno == "0"){
// 刷新當(dāng)前界面
location.reload()
}else {
$("#register-password-err").html(resp.errmsg)
$("#register-password-err").show()
}
}
})
打開 CSRFProtect 的代碼,并運(yùn)行測(cè)試 注:X-CSRFToken 這個(gè) key 是 CSRFProtect 這個(gè)類里面指定的激捏,具體請(qǐng)點(diǎn)擊進(jìn)入到此類查看源代碼