前言
并发漏洞各位师傅可能或多或少都接触过,大到并发支付,小到并发点赞、短信等。测试起来很非常简单,找到对应接口,直接tubor intruder一把梭!但是这种漏洞怎么产生的?开发是怎么写的代码?你真的了解它背后的原理吗?今天我想从一个开发的角度讲一讲,服务端到底是怎么实现的相关功能,开发人员又是怎么去规避并发逻辑漏洞的。
并发领取优惠券
领取优惠的功能在很多站点都存在,而且很多师傅也可能也都尝试过去并发领取优惠券,原本一个用户只允许领取一张,但是并发成功的话,可以绕过这个限制,领到几十甚至几百张。
下面以领取优惠券功能为例,讲一讲它的实现原理。
主要说明一下整个流程为主,不重要的部分就直接简化或省略了,如果是开发同学,直接跳过即可。
实体关系
首先这个功能涉及到两个实体,用户 以及 优惠券,在数据库里面可能就对应两张表,用户表 以及 优惠券表。同时,我们知道 每个用户一般是可以领多张优惠券的,而一张优惠券,又可以被多个用户领取。那么如果要记录它们之间的关系,就需要另外一张关系表来保存,记为 用户-优惠券关系表。
关系图
业务流程
那么有了上面的关系图表,那我们领取优惠券的流程就很简单了,比如从用户看到优惠券,到领取成功这个过程:
-
优惠券列表:在优惠券表中查询到可用的优惠券,返回给前端
-
用户点击领取操作:查询用户-优惠券关系表
a. 如果已经存在领取记录,则领取失败
b. 如果不存在领取记录,就往表中插入一条数据,记录这个用户已经领取过这张优惠券了,领取成功 -
返回前端告诉用户领取结果
示例图
分析上面的流程,为什么会产生并发漏洞呢?
多线程操作
正常来说,我们先查询用户有没有领取记录,再决定是否写入新记录,这个流程在串行操作下,是没有问题的。
什么是串行化操作?也就是每个请求都是依次处理,上一次请求处理结束后,才会处理下一个请求。
但是,我们知道现在的服务器基本都是多核,而为了更好的性能表现,现代的程序基本上都是使用多线程实现的。比如上面的例子,为了更好的吞吐量,肯定不可能将所有请求串行执行,所以每个请求进来肯定都是从线程池里面获取一个线程去执行,这意味着多个请求是并行在执行的。
而这在正常场景下,也是没有问题的:因为正常来说,一个用户点击领取按钮后,前端一般都会将按钮置为不可用,等请求成功返回后才会恢复。这就限制了用户的这个操作只能是串行的。
但是!!架不住师傅们直接绕过前端,抓包然后并发请求接口,这就会出现下面的情况:
同一个用户同一时间发起了3个请求:请求A、B在查询记录时,都发现没有记录,然后都进入到了下一步,写入领取记录。这时候就重复写入了2条记录。对于用户来说,就是一个只允许领取一次的优惠券,我们绕过限制领了2次。
请求C查询时,由于请求A已经写入记录了,所以会查到已经领取过,则返回领取失败。
上面就是并发逻辑漏洞产生原因,本质上就是因为多线程引发的线程安全问题。这种一般是由于开发的疏忽导致的,一般可能是缺乏经验,或者是开发进度紧张(来自开发人员的吐槽~),没有时间考虑安全性。
防御手段
那对于开发人员来说一般怎么避免这种情况呢?很简单:问题是由于多线程并行导致的,那直接让这部分操作串行化不就行了。
我们对查询记录、写入记录这两个操作加锁,也就是一旦要进行这两个操作,那就只能等上一次请求处理后,才允许处理下一个请求。
示例图
但是这会有一个问题,那如果有很多用户都进行领取操作,那有些用户不就要等很久?
这就涉及到锁粒度的问题,为了提高性能,我们只对同一个用户加锁,也就是如果是不同用户,还是并行执行的,但是如果是同一个用户进行领取优惠券操作,那么他就必须等上一次操作结束后,才能够进行下一次操作,这就避免了并发领取多个的问题。
加锁一定能解决问题吗?这里又涉及到本地锁以及分布式锁。有一些用户量不是很多的站点,可能服务端只部署了一个节点,那么他们可能一开始加锁都是用的本地锁(也就是在内存里面的)。但是后面随着用户量越来越多,开始多节点部署了,这时候本地锁就失效了(节点A加锁了,但是节点B没有,因为是在内存里面,所以无法共享)。这时候就要使用分布式锁。这里就不再细说了。
其它
其它还有一些什么数据库唯一约束、使用redis实现等就不再一一细说,但是本质上都是让这个操作串化,只是他们通过数据库或Redis等中间件来保障,而不是我们自己实现。
最后
这里只是以领取优惠券作为例子聊了一下并发问题,但是所有并发漏洞它的原理基本上都是类似的,本质上就是多线程环境下产生的线程安全问题。了解一些功能的实现原理,可以让我们更好地去理解漏洞产生的原因,以及从更多的角度去发现问题。我后面也会不定时地分享一些漏洞产生的原因以及开发是如何从设计或代码层面去防御漏洞的。
如果有什么疑问,也可以在评论区留言,互相交流~