django下防御race-condition漏洞竞争型漏洞
django下防御race condition漏洞(竞争型漏洞)
竞争型漏洞
概念
竞争型漏洞,也称为竞态条件漏洞(Race Condition Vulnerability),是由于程序在处理共享资源时,未能正确同步多个并发操作,导致执行结果依赖于这些操作的时序,从而可能被攻击者利用的安全漏洞。
假设有一个银行应用,处理转账的时候,如果两个转账操作同时进行,而余额检查没有正确同步,可能导致余额错误地被处理。例如,用户A有100元,同时发起两笔转账,各转出100元,如果系统没有正确锁定资源,可能两笔转账都通过,导致用户A的余额变成负数,这就是竞争条件导致的漏洞。
常见类型及示例
TOCTOU(Time-of-Check to Time-of-Use)
- 原理 :程序在检查资源状态后、使用资源前,攻击者篡改资源。
- 示例 :检查文件权限后,攻击者替换为恶意文件,导致高权限执行。
- 场景 :文件系统操作、权限验证。
资源争用
- 原理 :并发操作修改同一资源(如余额、库存)时未同步。
- 示例 :电商系统中,两用户同时购买最后一件商品,导致超卖。
- 场景 :金融交易、库存管理。
异步操作漏洞
- 原理 :回调或事件处理顺序不当引发状态混乱。
- 示例 :JavaScript中多个异步回调修改同一变量,导致数据错误。
- 场景 :前端应用、分布式任务处理。
下面介绍一个关于竞争性的漏洞
环境搭建
可以在资源中下载文件解压
将文件上传到linux下,我们修改文件按中的错误,
将.env.defaul修改为.env
修改文件中的DEBUG=True
然后进行如下操作
pip3 install -r requirements.txt
python3 manage.py migrate
python3 manage.py collectstatic
python3 manage.py createsuperuser
python3 manage.py runserver 0.0.0.0:8080
完成之后可以访问网站
漏洞复现
ucenter/1/
无锁无事务时的竞争攻击
class WithdrawView1(BaseWithdrawView):
success_url = reverse_lazy('ucenter:withdraw1')
def form_valid(self, form):
amount = form.cleaned_data['amount']
self.request.user.money -= amount
self.request.user.save()
models.WithdrawLog.objects.create(user=self.request.user, amount=amount)
return redirect(self.get_success_url())
利用bp抓包,当出现两次以上的扣款成功,就说明漏洞利用成功
竞争成功
查看日志
ucenter/2/
无锁有事务时的竞争攻击
class WithdrawView2(BaseWithdrawView):
success_url = reverse_lazy('ucenter:withdraw2')
@transaction.atomic
def form_valid(self, form):
amount = form.cleaned_data['amount']
self.request.user.money -= amount
self.request.user.save()
models.WithdrawLog.objects.create(user=self.request.user, amount=amount)
return redirect(self.get_success_url())
继续竞争
查看日志
竞争成功。
ucenter/3/
悲观锁加事务防御Race Condition
class WithdrawView3(BaseWithdrawView):
success_url = reverse_lazy('ucenter:withdraw3')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.user
return kwargs
@transaction.atomic
def dispatch(self, request, *args, **kwargs):
self.user = get_object_or_404(models.User.objects.select_for_update().all(), pk=self.request.user.pk)
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
amount = form.cleaned_data['amount']
self.user.money -= amount
self.user.save()
models.WithdrawLog.objects.create(user=self.user, amount=amount)
return redirect(self.get_success_url())
发现只有一次提款成功
ucenter/4/
乐观锁加事务防御Race Condition
class WithdrawView4(BaseWithdrawView):
success_url = reverse_lazy('ucenter:withdraw4')
@transaction.atomic
def form_valid(self, form):
amount = form.cleaned_data['amount']
rows = models.User.objects.filter(pk=self.request.user, money__gte=amount).update(money=F('money')-amount)
if rows > 0:
models.WithdrawLog.objects.create(user=self.request.user, amount=amount)
return redirect(self.get_success_url())
发现也只有一次成功
总结
悲观锁
概念
- 悲观锁假设并发冲突一定会发生,因此在访问数据时直接加锁,确保其他事务无法修改数据,直到当前事务完成。
- 悲观锁通常通过数据库的锁机制实现,如
SELECT ... FOR UPDATE
或SELECT ... LOCK IN SHARE MODE
。
特点
- 优点 :保证数据的一致性,适合写操作多的场景。
- 缺点 :加锁会降低并发性能,可能导致死锁。
使用场景
- 高并发写操作。
- 数据一致性要求高的场景。
实例
该例子需要实现一个转账操作,确保余额不会出现负数。
在
SELECT ... FOR UPDATE
中,账户A的记录被锁定,其他事务无法修改,直到当前事务提交或回滚。
-- 开启事务
START TRANSACTION;
-- 使用悲观锁锁定账户A的记录
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- 检查余额是否足够
IF (SELECT balance FROM accounts WHERE id = 1) >= 100 THEN
-- 扣除账户A的余额
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 增加账户B的余额
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
END IF;
-- 提交事务
COMMIT;
乐观锁
概念
- 乐观锁假设并发冲突很少发生,因此在访问数据时不加锁,而是在提交时检查数据是否被其他事务修改过。
- 乐观锁通常通过版本号(Version)或时间戳(Timestamp)实现。
特点
- 优点:提高并发性能,适合读操作多的场景。
- 缺点:如果冲突频繁,会导致大量事务回滚。
使用场景
- 高并发读操作。
- 冲突较少的场景。
实例
在乐观锁中,通过
version
字段检查数据是否被修改。如果
version
不匹配,说明数据已被其他事务修改,当前事务需要回滚并重试。
即,当用户A进行提款操作时,若存在两个以上的请求进程(查询的version为1),当某一个进程率先请求成功,version会自增1,其他的进程就无法查询到该版本号,从而不会执行账户操作,事务回滚。
-- 开启事务
START TRANSACTION;
-- 查询账户A的余额和版本号
SELECT balance, version FROM accounts WHERE id = 1;
-- 假设查询到的 balance = 500, version = 1
-- 检查余额是否足够
IF 500 >= 100 THEN
-- 扣除账户A的余额,并更新版本号
UPDATE accounts SET balance = balance - 100, version = version + 1 WHERE id = 1 AND version = 1;
-- 检查是否更新成功
IF ROW_COUNT() = 0 THEN
-- 版本号不匹配,说明数据已被其他事务修改,回滚事务
ROLLBACK;
ELSE
-- 增加账户B的余额
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 提交事务
COMMIT;
END IF;
END IF;