并发请求导致的业务处理安全风险及解决方案

blue 2013-12-25 11:51:00

0x00 背景


一段简单的购买程序,看起来没有任何问题。

剩余余额、商品库存、购买权限等判断面面俱到,从头到脚包装的严严实实。

但是为何人一多就频频漏点呐?何解?

0x01 问题分析


还是以商城购买为例,商城网站是web程序和数据库两部分,业务处理流程:

用户金额是否大于商品价格—>商品库存是否充足—>购买操作:生成订单—>扣除用户金额—>商品库存减1

流程的每一部分都是web与数据库打交道,查询或者操作数据库。

程序示例(PHP+MySQL)

$goods=$db->FirstRow("SELECT * FROM ".Tb('goods')." WHERE goods_id='{$goods_id}'");
if(empty($goods)) ShowError('商品不存在');
/* 金额是否充足 */
if($user->money<$goods['price'])  ShowError('金额不足,请充值');
/* 商品库存 */
if($goods['num']==0)  ShowError('库存不足');
/* 购买操作 begin */
//生成订单
CreateOrder($goods,$user,time());
$user->Update('money'=>$user->money-$goods['price']); //用户金额减少
$db->Execute("UPDATE ".Tb('goods')." SET num=num-1 WHERE goods_id='{$goods_id}'");//商品库存-1
ShowSuccess('购买成功');
/* 购买操作 end */

正常来看这个业务处理是没有问题的,下面想象下多人同时购买(并发请求,如秒杀活动)的情境可能会引发的问题?

如果一个用户同时有两次购买请求,一次购买已进行到添加订单但未扣除用户金额,另一次购买在第一步用户金额判断便不准确了。

当商品库存仅为1时,同时有多个请求,而当前没有一个请求走到商品库存减少位置,多次购买都能成功,而商城却无货可发。

总结来说,当有大量的购买操作同时进行,如果数据库的处理速度跟不上程序的请求速度,就会出现判断不准确的问题,造成用户以单个商品的金额购买多个商品、某些用户付款了但得不到商品等,算是一个安全风险。

0x02 解决方案:


核心思想:将一次业务处理流程(如购买操作)作为一个最小操作单元,同一时间只能有一个操作。

1.  整个操作加内存锁如在memcache里开始购买时设置购买状态为进行中购买结束后清除购买状态程序开始时即从memcache里判断是否有正在进行的购买操作如有则退出
2.  限制每个用户的购买间隔如10秒内仅允许购买一次最好也是放在内存里
3.  当然优化数据库及程序以加快处理速度也是有必要的

解决方案程序示例(PHP+MySQL+Memcached)

/**
 * 通过memcache解决并发购买问题
 */
$goods=$db->FirstRow("SELECT * FROM ".Tb('goods')." WHERE goods_id='{$goods_id}'");
if(empty($goods)) ShowError('商品不存在');
$mmc=memcache_init();
$lastBuyTime=$mmc->get('lastBuyTime_'.$user->userId);
if($lastBuyTime>0 && $lastBuyTime>time()-10)  ShowError('10秒内只能进行一次购买');
$buying=$mmc->get('buying');
if($buying==1)  ShowError('有正在进行的购买,请稍候');
/* 金额是否充足 */
if($user->money<$goods['price'])  ShowError('金额不足,请充值');
/* 商品库存 */
if($goods['num']==0)  ShowError('库存不足');
/* 购买操作 begin */
//生成订单
CreateOrder($goods,$user,time());
$user->Update('money'=>$user->money-$goods['price']); //用户金额减少
$db->Execute("UPDATE ".Tb('goods')." SET num=num-1 WHERE goods_id='{$goods_id}'");//商品库存-1
/* 购买操作 end */
$mmc->set('buying',0);
$mmc->set('lastBuyTime_'.$user->userId,time());
ShowSuccess('购买成功');

评论

Z

ziwen 2013-12-25 12:07:15

尼玛这应该算是逻辑漏洞吧....

园长 2013-12-25 12:10:26

业务逻辑缺陷,并发=RMB

W

wuxianjun 2013-12-25 14:51:54

亲,下单、扣款、减库存在同一个事务里吗?
你这程序在高并发的情况下库存会出现超卖的情况。

齐迹 2013-12-25 15:44:16

@blue mc 也是有可能延迟的 所以目前最好还是只能利用事务。

齐迹 2013-12-25 15:45:36

还有就是异步 用队列来实现。

B

blue 2013-12-25 16:24:25

将操作都放到数据库里肯定最好,之前还看过一程序所有的操作都是存储过程,我这只是描述一种有安全风险的情况和能够快速部署的解决方案

B

blue 2013-12-25 16:40:33

就是为了解决高并发带来的问题,下单、扣款、减库即使在一个事务里,未处理完时前面的判断也会被绕过,而且像一些通用的电商程序,因为不保证数据库是否支持事务而没有使用事务~

齐迹 2013-12-25 17:14:05

开启事务都是是会锁表的。理论上不存在绕过的可能。有一个情况我们之前遇到过 当查询的时候走的从库 会导致绕过。

小胖子 2013-12-25 22:05:46

用小米的方式来处理,哈哈哈,等你们先抢单,再支付,抢单成功的再支付,队列来解决这个问题。

M

Mr.Anderson 2013-12-26 10:54:17

cool stuff.

W

wuxianjun 2013-12-26 14:22:38

不同用户的情况下:
A线程 ----> 获取goods当时库存是10个,刚好执行到18行.
B线程 ----> 因为A线程执行18行,还没执行减少库存代码,B线程执行4行代码的时候从数据库获取goods的库存还是10个。
而这样的情况,假如A线程买了9个走了,实际库存只有一个,这样B线程获取的数据就有问题了。
这样会导致库存超卖。

Z

zsmynl 2013-12-26 16:33:28

学习,学习~

X

xsser 2013-12-30 18:22:11

一看洞主就是吃过亏的人

N

nyannyannyan 2013-12-30 22:05:43

加了独占锁...B线程想获取goods记录存必须wait....
这是教开发并发程序最开始教的producer-consumer模型...有一套很成型的semaphore的数据结构来解决

_

_Evil 2014-01-02 18:01:03

楼上也吃过短短的亏啊 忘记了?

C

clzzy 2014-01-07 14:37:36

尼玛这应该算是逻辑漏洞吧....

乌帽子 2014-01-11 16:06:02

http://ww3.sinaimg.cn/bmiddle/53993e84jw1ecf2e1phf8j20c89zl7wi.jpg

浅蓝 2015-03-09 12:35:45

学习了

F

Fire ant 2015-03-09 12:47:22

发家致富的时刻到了

炊烟 2015-03-13 17:52:46

库币不足

浮世浮城 2015-03-24 16:31:54

膜拜浅蓝大牛

路人甲 2015-03-26 17:02:30

楼上的同学们仔细看了吗? 这个解决方案漏洞重重啊
1 没有设置buying=1的地方? 还是我没找到?
2 第9行和第10行之间有执行时间间隔,如果原来是0,那这个瞬间的所有请求都会通过
3 为什么把lastBuyTime_的设置放到最后? 应该放到页首吧,第8行后面
4 用秒来计算10秒, 那第0秒或10秒后任意1秒内的所有请求都通过, 1秒内弄个1000QPS没问题吧?

T

tnaw 2015-04-11 11:37:11

wooyun强项是找漏洞,不是修复。
解决方案一点也不好。 起码应该有事务吧,再高级点,有命令模式吧。

B

blue

我心中有猛虎,细嗅蔷薇。

twitter weibo github wechat

随机分类

其他 文章:95 篇
SQL注入 文章:39 篇
数据分析与机器学习 文章:12 篇
浏览器安全 文章:36 篇
逆向安全 文章:70 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录