假设我使用CodeIgniter / PHP和MySQL创建了一个在线银行系统,并从我的银行账户中提取以下资金:
["value1", "value2", "value3"]
首先,我们检查用户是否可以执行撤销,然后从帐户中扣除,然后我们添加到钱包。
问题在于我可以在几乎完全相同的时间发出多个请求(这基本上是使用function withdraw($user_id, $amount) {
$amount = (int)$amount;
// make sure we have enough in the bank account
$balance = $this->db->where('user_id', $user_id)
->get('bank_account')->balance;
if ($balance < $amount) {
return false;
}
// take the money out of the bank
$this->db->where('user_id', $user_id)
->set('balance', 'balance-'.$amount, false)
->update('bank_account');
// put the money in the wallet
$this->db->where('user_id', $user_id)
->set('balance', 'balance+'.$amount, false)
->update('wallet');
return true;
}
)。每个请求都有自己的线程,所有线程都同时运行。因此,每个人都会检查我的银行是否有足够的金额(我这样做),然后每个人都执行提款。结果,如果我开始100的余额,并且我发出两个curl
请求导致同时撤回100,那么我最终在我的钱包里有200个,在我的银行里有100个帐户,这是不可能的。
解决此类TOCTOU漏洞的正确“CodeIgniter”方法是什么?
答案 0 :(得分:3)
我将bank_account
和wallet
表的存储引擎设置为具有事务支持的InnoDB,然后在SELECT语句中包含FOR UPDATE子句以锁定交易期间的bank_account
表。
代码将类似于以下内容。
function withdraw($user_id, $amount) {
$amount = (int)$amount;
$this->db->trans_start();
$query = $this->db->query("SELECT * FROM bank_account WHERE user_id = $user_id AND balance >= $amount FOR UPDATE");
if($query->num_rows() === 0) {
$this->db->trans_complete();
return false;
}
$this->db->where('user_id', $user_id)
->set('balance', 'balance-'.$amount, false)
->update('bank_account');
$this->db->where('user_id', $user_id)
->set('balance', 'balance+'.$amount, false)
->update('wallet');
$this->db->trans_complete();
return true;
}
答案 1 :(得分:0)
我在寻找的是table locking。
为了使这个函数安全,我需要在函数的开头锁定表,然后在结束时释放它们:
function withdraw($user_id, $amount) {
$amount = (int)$amount;
// lock the needed tables
$this->db->query('lock tables bank_account write, wallet write');
// make sure we have enough in the bank account
$balance = $this->db->where('user_id', $user_id)
->get('bank_account')->balance;
if ($balance < $amount) {
// release the locks
$this->db->query('unlock tables');
return false;
}
// take the money out of the bank
$this->db->where('user_id', $user_id)
->set('balance', 'balance-'.$amount, false)
->update('bank_account');
// put the money in the wallet
$this->db->where('user_id', $user_id)
->set('balance', 'balance+'.$amount, false)
->update('wallet');
// release the locks
$this->db->query('unlock tables');
return true;
}
这使得任何其他MySQL连接写入所述表的任何尝试都会挂起,直到锁被释放为止。