Web后端学习笔记 Flask(10)CSRF攻击原理
CSRF(Cross Site Request Forgery,跨站域请求伪造)是一种网络的攻击方式,它在2007年曾被列为互联网20大安全隐患之一。
CSRF攻击的原理:
网站是通过cookie实现登录功能的,而cookie只要存在浏览器中,那么浏览器在访问这个cookie所对应的网站的时候,就会自动的携带cookie信息到服务器上去。那么这时候就存在一个漏洞,如果你在访问网站未退出的情况下,又访问了一个病毒网站,那么这个网站可以在网页代码中插入JS代码,使用JS代码给其他服务器(未退出的网站)发送请求(例如ICBC转账请求)。因为在发送请求的时候,请求也是由浏览器发送出去的,所以浏览器会自动把cookie信息发送给对应的服务器(ICBC),所以服务器就不知道这个请求是伪造的,就被欺骗过去了。从而达到在用户不知情的情况下,给服务器发送了转账请求。
原理图如下所示:
CSRF攻击防御:
CSRF攻击的要点就是在向服务器发送请求的时候,相应的cookie会自动地发送给对应的服务器。不知道这个请求是用户发起的还是伪造的。这时候,可以在用户每次访问有表单的页面的时候,在网页源代码中添加一个随机的字符串,叫csrf_token,在cookie中也加入一个相同值的csrf_token字符串。以后在给服务器发送请求的时候,必须在body以及cookie中都携带csrf_tooken,服务器只有检测到cookie中的csrf_token和body中的csrf_token相同,才会认为这个请求是正常的,否则就是伪造的。
下面通过一个实例实现CSRF攻击:
1. 首先编写一个类似于ICBC转账网站:
主要是简单地实现的是注册,登陆,转账功能
首先实现数据库映射,采用的是flask-migrate,以及flask-script
实现数据库db
配置文件: config.py
# -*- coding: utf-8 -*- import os from datetime import timedelta HOST_NAME = "127.0.0.1" PORT = "3306" DATABASE = "icbc" USERNAME = "root" PASSWORD = "root1234" # dialect+driver://username:password@host:port/database DB_URI = "mysql+pymysql://{username}:{password}@{host}:{port}/{database}".format( username=USERNAME, password=PASSWORD, host=HOST_NAME, port=PORT, database=DATABASE ) SQLALCHEMY_DATABASE_URI = DB_URI SQLALCHEMY_TRACK_MODIFICATIONS = None TEMPLATE_AUTO_RELOAD = True DEBUG = True SECRET_KEY = os.urandom(24) # 设置flask中session加密的字符串 24长度的加密字符串 PERMANENT_SESSION_LIFETIME = timedelta(days=1)
实现db:
# -*- coding: utf-8 -*- from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy()
models.py中,定义数据库ORM
# -*- coding: utf-8 -*- from exts import db class User(db.Model): __tablename__ = "user" id = db.Column(db.Integer, primary_key=True, autoincrement=True) email = db.Column(db.String(50), nullable=False) username = db.Column(db.String(50), nullable=False) password = db.Column(db.String(50), nullable=False) deposit = db.Column(db.Float, default=0)
manager.py中,进行数据库迁移,这里要用到flask-migrate和flask-script
# -*- coding: utf-8 -*- from flask_script import Manager from app import app from flask_migrate import MigrateCommand, Migrate from exts import db from models import User # 只需要导入模型即可,flask会自动进行检测 manager = Manager(app) Migrate(app=app, db=db) manager.add_command("db", MigrateCommand) if __name__ == "__main__": manager.run()
通过flask-migrate中的命令行实现数据库迁移:
python mamanger.py db init 初始化alembic仓库
python manager.py db migrate 生成迁移脚本
python manager.py db upgrade 完成数据库迁移
定义前端页面:
index.html 首页
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ICBC首页</title> </head> <body> <h1>ICBC欢迎你</h1> <ul> <li><a href="{{ url_for("register") }}">立即注册</a></li> <li><a href="{{ url_for("login") }}">立即登陆</a></li> <li><a href="{{ url_for("transfer") }}">立即转账</a></li> </ul> </body> </html>
login.html 登陆页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ICBC登陆</title> </head> <body> <form action="" method="post"> <table> <tbody> <tr> <td>邮箱: </td> <td><input type="email" name="email"></td> </tr> <tr> <td>密码: </td> <td><input type="password" name="password"></td> </tr> <tr> <td></td> <td><input type="submit" value="登录"></td> </tr> </tbody> </table> </form> </body> </html>
register.html 注册页面:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ICBC用户注册</title> </head> <body> <form action="" method="post"> <table> <tbody> <tr> <td>邮箱: </td> <td><input type="email" name="email"></td> </tr> <tr> <td>用户名: </td> <td><input type="text" name="username"></td> </tr> <tr> <td>余额: </td> <td><input type="text" name="deposit"></td> </tr> <tr> <td>密码: </td> <td><input type="password" name="password"></td> </tr> <tr> <td>重复密码: </td> <td><input type="password" name="repeat_password"></td> </tr> <tr> <td></td> <td><input type="submit" value="注册"></td> </tr> </tbody> </table> </form> </body> </html>
transfer.html 转账页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ICBC转账页面</title> </head> <body> <form action="" method="post"> <table> <tbody> <tr> <td>转到账号: </td> <td><input type="email" name="transfer_account" placeholder="邮箱"></td> </tr> <tr> <td>转账金额: </td> <td><input type="text" name="transfer_money"></td> </tr> <tr> <td></td> <td><input type="submit" value="转账"></td> </tr> </tbody> </table> </form> </body> </html>
在完成页面后,需要定义表单验证模块,对注册,登陆,以及转账的数据进行验证,forms.py
# -*- coding: utf-8 -*- from wtforms import Form, StringField, FloatField from wtforms.validators import Email, Length, EqualTo, InputRequired from models import User from exts import db class Registry(Form): email = StringField(validators=[Email()]) username = StringField(validators=[Length(min=4, max=10)]) password = StringField(validators=[Length(min=5, max=12)]) repeat_password = StringField(validators=[EqualTo("password")]) deposit = FloatField(validators=[InputRequired()]) class Login(Form): email = StringField(validators=[Email()]) password = StringField(validators=[Length(min=5, max=12)]) # 可以先在表单这里进行验证, 自定义验证器 # def validate(self): # result = super(Login, self).validate() # 先调用父类的validator,看能否通过验证 # if not result: # return False # # 通过查询数据库验证用户 # email = self.email.data # password = self.password.data # user = db.session.query(User).filter(User.email == email, # User.password == password).first() # if user: # return True # else: # self.email.errors.append("邮箱或密码错误") # return False class Transfer(Form): transfer_account = StringField(validators=[Email()]) transfer_money = FloatField(validators=[InputRequired()])
因为表单登录涉及到get和post方法,所以这里推荐使用类视图实现:app.py定义视图函数
from flask import Flask, render_template, views, request, session import config from forms import Registry, Login, Transfer from exts import db from models import User from auth import login_required app = Flask(__name__) app.config.from_object(config) db.init_app(app=app) @app.route('/') def hello_world(): return render_template("html/index.html") class RegisterView(views.MethodView): def get(self): """ 定义get方法执行的操作 :return: """ return render_template("html/register.html") def post(self): """ 定义post方法执行的操作 :return: """ form = Registry(request.form) if form.validate(): email = form.email.data username = form.username.data password = form.password.data deposit = form.deposit.data # 注册信息保存到数据库 user = User(email=email, username=username, password=password, deposit=deposit) db.session.add(user) db.session.commit() return "注册成功" else: print(form.errors) return "注册失败" class LoginView(views.MethodView): def get(self): """ 定义get方法下的操作 :return: """ return render_template("html/login.html") def post(self): """ 定义post方法下的操作 :return: """ form = Login(request.form) if form.validate(): email = form.email.data password = form.password.data user = db.session.query(User).filter(User.email == email, User.password == password).first() if user: # 通过session来完成 session["user_id"] = user.id session.permanent = True return "登陆成功" else: return "邮箱或密码错误" else: print(form.errors) return "登陆失败" class TransferView(views.MethodView): decorators = [login_required] def get(self): return render_template("html/transfer.html") def post(self): form = Transfer(request.form) if form.validate(): transfer_account = form.transfer_account.data transfer_money = form.transfer_money.data # 这里已经通过表单验证转换为float类型 user = db.session.query(User).filter(User.email == transfer_account).first() if user: current_account_id = session.get("user_id") # 获取当前登陆用户的id current_user = db.session.query(User).filter(User.id == current_account_id).first() if current_user.deposit > transfer_money: # 可以进行转账 user.deposit = user.deposit + transfer_money current_user.deposit = current_user.deposit - transfer_money db.session.add_all([user, current_user]) db.session.commit() return "转账成功" else: return "余额不足" else: return "用户不存在" else: return "数据填写不正确" app.add_url_rule("/register/", view_func=RegisterView.as_view("register")) app.add_url_rule("/login/", view_func=LoginView.as_view("login")) app.add_url_rule("/transfer/", view_func=TransferView.as_view("transfer")) if __name__ == '__main__': app.run()
还有一点需要注意的,在正常情况下,只有登录状态下,才可以访问转账页面,所以这里可以通过定义装饰器,来实现这一功能。auth.py
# -*- coding: utf-8 -*- # 做登录限制,有些页面只有登录之后才能访问 # 通过定义装饰器实现 from functools import wraps from flask import session, redirect, url_for def login_required(func): @wraps(func) # 防止传入的函数的一些签名丢失 def wrapper(*args, **kwargs): if session.get("user_id"): # 当前处于登陆状态 return func(*args, **kwargs) else: return redirect(url_for("login")) return wrapper
这样,就实现了一个转账网站的简易功能。
给网站添加CSRF防御:
csrf_token的原理是:
以用户转账页面为例:在用户请求转账的时候,服务器准备返回转账页面,但是在返回转账页面之前,服务器会做两件事情:
1. 在cookie中添加csrf_token, 一个唯一的字符串,然后将在返回请求页面的同时,会将cookie存储到浏览器。
2. 在转账页面当中也添加一个相同的csrf_token, 然后在将页面返回到浏览器。
在用户输入完相关的转账信息,在点击提交按钮之后,就会将请求发送给服务器。同时浏览器也会将cookie发送给服务器。所以服务器会将表单当中的csrf_token和cookie中的csrf_token进行对比,如果两者相同,则表示通过验证。否则,这个请求就是一个伪造的请求。
此时恶意网站是无法去伪造页面中csrf_token的,因为每次请求页面的时候,csrf_token都是不同的。
在flask框架中,已有CSRF防御的相应机制。使用非常简单。
1. 直接导入CSRFProtect
2. CSRFProtect绑定APP
3. 在相应的表单页面,需要添加一个input标签,因为这个标签只是存储后端返回的csrf_token,不会显示在返回的页面上,所以需要设置type="hidden"
【注】服务器返回给页面的csrf_token和返回给浏览器中cookie的csrf_token虽然是同一个值,但是分别经过了不同的转换方法,所以这两者看起不是相同的字符串(但实质上在后端经过转换后还是同一个字符串)
AJAX处理CSRF漏洞
通过ajax提交表单,定义login.js文件,这里获取表单元素使用了jQuery
// 整个文档加载完毕后才会执行这个函数 window.onload = function() {} $(function () { $('#submit').click(function (event) { event.preventDefault(); // 点击按钮后此时不会再提交,而是执行后面的代码 let email = $('input[name=email]').val(); let password = $('input[name=password]').val(); let csrf_token = $('input[name=csrf_token]').val(); // 通过ajax的post方法提交数据 $.post( { "url": "/login/", // 同一域名下,前面的部分可以省略 'data': { "email": email, "password": password, "csrf_token": csrf_token }, 'success': function (data) { console.log(data) }, 'fail': function (error) { console.log(error) } } ) }) });
在flask中,一般推荐将存储csrf_token的input标签放到head中的meta标签中,这样做的好处是,例如表单需要继承模板,则只需要在父模板中写好csrf_token即可,子模版无论是否用到csrf_token,都会拥有csrf_token。
则在login.js中获取csrf_token:
在用ajax提交数据的时候,也可以不把csrf_token放在提交的数据中,而是放在请求头中:
// 整个文档加载完毕后才会执行这个函数 window.onload = function() {} $(function () { $('#submit').click(function (event) { event.preventDefault(); // 点击按钮后此时不会再提交,而是执行后面的代码 let email = $('input[name=email]').val(); let password = $('input[name=password]').val(); // let csrf_token = $('input[name=csrf_token]').val(); let csrf_token = $('meta[name=csrf_token]').attr('content'); // 在进行Ajax请求之前,将csrf_token放到请求头中 $.ajaxSetup( { "beforeSend": function(xhr, settings) { if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrf_token) } } } ); $.post( { "url": "/login/", // 同一域名下,前面的部分可以省略 'data': { "email": email, "password": password //"csrf_token": csrf_token }, 'success': function (data) { console.log(data) }, 'fail': function (error) { console.log(error) } } ) }) });
-----------------------------------------------------------------------------------------------------------------------------------
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)