今天下午在v2ex上看到一个帖子,讲述自己因为忘记加分布式锁导致了公司的损失:
![c1684f9e019bf8a3ebff1958e90a3b5d.png](https://img-blog.csdnimg.cn/img_convert/c1684f9e019bf8a3ebff1958e90a3b5d.png)
我曾在《从Pwnhub诞生聊Django安全编码》一文中描述过关于商城逻辑所涉及的安全问题,其中就包含并发漏洞(Race Condition)的防御,但当时说的比较简洁,也没有演示实际的攻击过程与危害。今天就以v2ex上这个帖子的场景来讲讲,常见的存在漏洞的Django代码,与我们如何正确防御竞争漏洞的方法。
0x01 Playground搭建
首先,使用我这个Django-Cookiecutter脚手架创建一个项目,项目名是Race Condition Playground。
创建两个新的Model:
class User(AbstractUser):
username = models.CharField('username', max_length=256)
email = models.EmailField('email', blank=True, unique=True)
money = models.IntegerField('money', default=0)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
class Meta(AbstractUser.Meta):
swappable = 'AUTH_USER_MODEL'
verbose_name = 'user'
verbose_name_plural = verbose_name
def __str__(self):
return self.username
class WithdrawLog(models.Model):
user = models.ForeignKey('User', verbose_name='user', on_delete=models.SET_NULL, null=True)
amount = models.IntegerField('amount')
created_time = models.DateTimeField('created time', auto_now_add=True)
last_modify_time = models.DateTimeField('last modify time', auto_now=True)
class Meta:
verbose_name = 'withdraw log'
verbose_name_plural = 'withdraw logs'
def __str__(self):
return str(self.created_time)
一个是User表,用以储存用户,其中money字段是这个用户的余额;一个是WithdrawLog表,用以储存提取的日志。我们假设公司财务会根据这个日志表来向用户打款,那么只要成功在这个表中插入记录,则说明攻击成功。
然后,我们编写一个WithdrawForm,其字段amount,表示用户此时想提取的余额,必须是整数:
class WithdrawForm(forms.Form):
amount = forms.IntegerField(min_value=1)
def __init__(self, *args, **kwa