Web后端学习笔记 Flask(10)CSRF攻击原理

CSRF(Cross Site Request Forgery,跨站域请求伪造)是一种网络的攻击方式,它在2007年曾被列为互联网20大安全隐患之一。

CSRF攻击的原理:

网站是通过cookie实现登录功能的,而cookie只要存在浏览器中,那么浏览器在访问这个cookie所对应的网站的时候,就会自动的携带cookie信息到服务器上去。那么这时候就存在一个漏洞,如果你在访问网站未退出的情况下,又访问了一个病毒网站,那么这个网站可以在网页代码中插入JS代码,使用JS代码给其他服务器(未退出的网站)发送请求(例如ICBC转账请求)。因为在发送请求的时候,请求也是由浏览器发送出去的,所以浏览器会自动把cookie信息发送给对应的服务器(ICBC),所以服务器就不知道这个请求是伪造的,就被欺骗过去了。从而达到在用户不知情的情况下,给服务器发送了转账请求。

原理图如下所示:

Web后端学习笔记 Flask(10)CSRF攻击原理_第1张图片

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   首页




    
    ICBC首页


    

ICBC欢迎你

login.html 登陆页面




    
    ICBC登陆


    
邮箱:
密码:

register.html 注册页面:




    
    ICBC用户注册


    
邮箱:
用户名:
余额:
密码:
重复密码:

transfer.html  转账页面




    
    ICBC转账页面


    
转到账号:
转账金额:

在完成页面后,需要定义表单验证模块,对注册,登陆,以及转账的数据进行验证,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

Web后端学习笔记 Flask(10)CSRF攻击原理_第2张图片

3. 在相应的表单页面,需要添加一个input标签,因为这个标签只是存储后端返回的csrf_token,不会显示在返回的页面上,所以需要设置type="hidden"

Web后端学习笔记 Flask(10)CSRF攻击原理_第3张图片

【注】服务器返回给页面的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)
                }
            }
        )
   })
});

-----------------------------------------------------------------------------------------------------------------------------------

你可能感兴趣的:(后端学习)