一次锁的不正确使用导致的安全漏洞

梦康 2021-12-09 22:07:10 618

昨天收到一个安全漏洞,说是有个投票接口存在刷票的情况,需要紧急看下。

我最初偷懒在本地搞个单元测试跑了下,发现没有出现并发写的情况,只有一个线程通过,其他的都锁住了。

class voteActionRunnable implements Runnable {

    @Override
    public void run() {
        ...
        // dataResult 是执行投票的方法的返回值
        System.out.println(JSON.toJSONString(dataResult));
    }
}

@Test
public void voteAction(){

    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(new voteActionRunnable());
        thread.run();
    }
}

既然安全提了肯定不是空穴来风,还是完全按照真实的请求场景来,从客户端并发发起 http 请求开始。因为需要保存比较多的上下文参数,还要很直观的查看过滤结果,我就试着用 jmeter 来弄下。确实挺香。

下载安装

https://jmeter.apache.org/download_jmeter.cgi
下载一个编译好了的压缩版,不用安装了,解压即可使用
屏幕快照 2021-12-09 上午10.08.31.png

启动

怎么启动呢,看这里 https://jmeter.apache.org/usermanual/get-started.html#running 也就是说这个二进制包是windows 和 unix 类系统通用的,不区分环境。
解压完后,进入 bin 目录,因为我是 mac 环境就点击 jmeter bin 文件,如果是 windows 点击jmeter.bat,然后就会弹出一个 GUI 界面了
屏幕快照 2021-12-09 上午10.11.55.png
jmeter国际化做的不错,设置下语言,使用更方便
屏幕快照 2021-12-09 上午10.20.33.png
然后点击 文件-> 新建一个测试计划,然后就是全中文了

配置测试计划

添加线程组

屏幕快照 2021-12-09 上午10.26.20.png
设置 20 个线程来模拟用户并发请求或者攻击
屏幕快照 2021-12-09 上午10.27.51.png

线程组下添加 HTTP 请求

屏幕快照 2021-12-09 上午10.28.47.png
屏幕快照 2021-12-09 上午10.38.53.png

测试计划增加 HTTP 信息头管理器

屏幕快照 2021-12-09 上午10.47.40.png
直接从控制台复制粘贴
屏幕快照 2021-12-09 上午10.49.57.png
image.png

测试计划增加察看结果树

屏幕快照 2021-12-09 上午10.55.11.png
image.png

执行测试计划

image.png
然后可以在这里查看测试的结果,并且去做些匹配检查,比较方便。这只是这种场景的测试,功能非常强大,可以玩玩。

回归业务

在使用 jmeter 之后发现确实出现了刷票的情况,比如业务上只允许一个人一天只能投n票,20个线程并发,就发现多了很多票。最后发现(实际代码写的比较长,简化之后)类似于这样的 bug

try{
    // 按照投票行为+人+投票对象加锁
    boolean lock = tairService.lock(lockCacheKey, expireTime);
    if(!lock){
        log.error("lock not release,lockKey:{}",lockCacheKey);
        return xxx;
    }
    
    // 获取到锁之后的业务逻辑
    ...
} catch (Exception e) {
    ...
} finally {
    boolean unLock = tairService.unLock(lockCacheKey, expireTime);
    if(!unLock){
        log.error("lock release failed,lockKey:{}",lockCacheKey);
    }
}

以四个并发请求为场景,可能出现如下的情况:

a 请求先拿到分布式锁,然后 b 请求拿不到锁,但是因为有 finally 操作,return 的同时会把 a 请求的锁释放了;
然后 c 请求进来就又拿到了同样的锁,继续投票;
a 请求如果先执行完,又把这个锁释放了,c 还没执行完,d 请求进来又能拿到锁,继续投票。

为什么在单元测试的时候没有发现呢?

可能是单元测试线程的并发性更高,而 jmeter 网络发起存在时差,就正好触发了。

解决方案

  1. 加锁那块和投票处理逻辑分开,不要共用 finally。
  2. 锁里面可以加随机值,防止释放锁的时候出现“越权”。这需要考分布式锁的命令的原子性。关于分布式的文章很大大家可以自行搜索,推荐框架 redisson 框架的解决方案。