banner
raye~

Raye's Journey

且趁闲身未老,尽放我、些子疏狂。
medium
tg_channel
telegram
twitter
douban
github
email
nintendo switch
playstation
steam_profiles
zhihu

How to defend against concurrency vulnerabilities, using Flask/Django as an example

Let me improve it, let's start a new project.

Race condition vulnerability, also known as a race condition bug, is most likely to occur in business scenarios such as lottery, orders, points, and withdrawals. After all, we often write code in a single thread, which makes this vulnerability very hidden. However, once it occurs, the harm is particularly great. Therefore, let's look at how to defend against race condition vulnerabilities from the perspective of actual coding scenarios.

To simplify the development process and demonstrate the vulnerability, we will use two web frameworks in Python as examples: Flask and Django. Why choose these two? Because they are the most popular web frameworks in Python, and they also have different implementations in terms of ORM. Flask uses SQLAlchemy, while Django uses its own Django ORM.

Let's start with Flask.

Here, let's take a look at a business scenario of user withdrawal. First, design two models:

# User
class User(db.Model, UserMixin):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(256), unique=True, nullable=False)
    email = db.Column(db.String(256), unique=True, nullable=False)
    password = db.Column(db.String(128), nullable=False)
    money = db.Column(db.Integer, default=0)

# Withdrawal
class WithdrawLog(db.Model):
    __tablename__ = 'withdraw_logs'
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    amount = db.Column(db.Integer, nullable=False)
    created_time = db.Column(db.DateTime, default=datetime.utcnow)

The code is easy to understand. User stores the user's account information and balance, while WithdrawLog is associated with the user table as a foreign key. There will be a record for each withdrawal made by the user.

Since Flask does not have a built-in admin interface, we will use flask-admin to create a simple backend and add user login authentication.

# Custom AdminIndexView with authentication
class MyAdminIndexView(AdminIndexView):
    @expose('/')
    @login_required
    def index(self):
        return super(MyAdminIndexView, self).index()

# Custom ModelView with authentication
class MyModelView(ModelView):
    def is_accessible(self):
        return current_user.is_authenticated

    def inaccessible_callback(self, name, **kwargs):
        return redirect(url_for('login', next=request.url))

    def on_model_change(self, form, model, is_created):
        if is_created or form.password.data != '':
            model.set_password(form.password.data)
        super(MyModelView, self).on_model_change(form, model, is_created)

admin = Admin(app, name='My App Admin', template_mode='bootstrap4', index_view=MyAdminIndexView())
admin.add_view(MyModelView(User, db.session))
admin.add_view(MyModelView(WithdrawLog, db.session))

Now we can manage users in the backend. Let's assign 10 money to the user.

Race condition without locks or transactions#

@app.route('/withdraw1', methods=['GET','POST'])
@login_required
def withdraw1():
    if request.method == 'POST':
        amount = int(request.form['amount'])
        if current_user.money >= amount:
            current_user.money -= amount
            db.session.add(WithdrawLog(user_id=current_user.id, amount=amount))
            db.session.commit()
            flash('Withdrawal successful')
            return redirect(url_for('index'))
        else:
            flash('Insufficient funds')
    return render_template('withdraw.html')

At this point, there is no defense mechanism. If multiple requests arrive at the server, before the previous request deducts the money, the money in the next request is still the original amount, which leads to duplicate deductions.

Let's use yakit for fuzzing.

yakit is a powerful penetration testing tool, supporting domestic products!

Use {{repeat(100)}} to repeat sending 100 packets. Set the number of threads to 200. You can see that 4 302 redirects appear.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.