關於web資金系統提現安全保護,防止極快的重復並發請求導致重復提現的解決思路


關於WEB金融系統中的提現安全問題很多人沒有深入思想,導致有漏洞,常常會遇到有些人遇到被攻擊到導資金損失的麻煩,     其實要徹底解決重復並發請求 導致重復提現問題,是需要花點心思的,並沒有看起來的那么 簡單,即使是最直觀簡單的語句都是有漏洞的比如:

-----------------------------------------場景1--------------------

發現很多朋友的項目一個漏洞:先為一賬戶充值100元,然后瞬間發送10次提現請求(都是提現100,提現接口是有做余額不足校驗的),其中大約有四五次都是成功的,剩下的會報余額不足。期望是,只有一次可以成功完成提現,分析到能部分請求能通過余額不足校驗原因是,由於是瞬間發出的提現請求,這些請求中拿到的余額數據都是余額扣減之前的數據。

以上場景可以提煉出兩個關鍵步驟:

  1. 查詢余額並校驗,select * from account where user_id = 123;
  2. 扣減余額並支付,update account set balance...

根據以上步驟,可知:1.在兩條SQL語句執行的中間這段時間,由於重復請求攻擊,可能會出現多次請求的第一步操作成功,並繼續執行第二步,最后導致資金損失。2.由於第一步操作是查詢操作,沒有數據庫會限制重復讀取數據

-----------------------------------------場景2----------------------------------

重復提交,表面上是重復提交,威力不大,但實際。。。我們來分析分析:
假設一個用戶,余額100,平台恰好有個提現的地方,理所當然用戶最多只能提取100元。
我們來分析下程序在生成提現數據的過程:
開啟事務;
用戶發起一次提現請求,到達應用后,程序判斷用戶余額是否夠用,如果不夠就跳出事務了;
然后扣除100元,
然后再提現數據表中插入一條數據,
到這里還沒結束,因為事務還沒提交,當上面進行順利時,到達這里就應該commit提交了,如果上面操作任何一步異常,就rollback回滾了。
看起來挺完美的過程,其實!弱暴了!
為啥?
假如用戶發起兩個請求,而且同一時間(1/1000秒級)請求到服務器,
再走一次上面的邏輯:
請求一達到服務器 請求二達到服務器
開啟事務 開啟事務
余額檢查->通過 余額檢查->通過
扣除余額->done 扣除余額->done
插入提現記錄->done 插入提現記錄->done
提交->commit(); 提交->commit();
兩邊幾乎同時進行一樣的操作,為什么沒被攔截掉只處理一個請求呢?因為余額檢查時,別的請求的事務未提交,在此請求內select的數據還未生效,所以兩個請求處理都通過了檢查。
那怎么防御呢?
token?
扯J8蛋!token用來防御這原子級別的攻擊?別說session了,即使你重寫php底層,讓session動態調用php的內存也無濟於事。原因自己腦補;
隊列是終極解決方案。
然后有一個臨時方案,提現的表中肯定會有time/datetime之類的字段,在建表時將這個表中的time/datetime + userId 設置為聯合主鍵,然后事務在插入提現數據時,因為時間同一秒且同一用戶所以數據沖突,只會成功一條,然后事務報錯啟動回滾,近乎完美。唯一的瑕疵就是假如前后誤差1ms, 然后恰好前一個時間是xxxx1,后一個時間是xxxx2,這樣就扯痛蛋了。。。千分之一的概率。

-----------------------------------------原因-----------------------------------

有人人甚至認為無解 ,其實是對數據庫理解不夠深,如事務級別(臟讀,讀提交,不可重復讀,序列化級,快照級,)、並發機制、鎖(共享鎖,更新鎖,X獨占鎖,行級鎖,頁級鎖、意向鎖),這么多底層知識有足夠的理解才能解決這個問題,因為這些方便資料很少,願意花精力去研究的人更不多,更郁悶的是微軟數據庫對查詢作了優化,文檔和實際執行效果是不一樣的,比如微軟文檔明文寫着,共享鎖 與更新鎖是可以相關排斥的,select語句默認是發布hold共享鎖,如果你真信就完了,你實際執行結果是共享鎖和updlock不會排斥,除非你顯示指定,select * from  account with(holdlock)  文檔和實際不一致,只有遇到坑后請求微軟技術支持才他技術人員才知道你,微軟對select做了特別優勢默認不是被排斥很多鎖的,瞬間被坑,當年還記有個攜程的主程序員不懂鎖亂用,給一個查詢加了with(nolock), 訂票資金出現重大事故教訓。 所以我提供以下幾個常用的解決方法。不是不可能其實也很簡單。

數據在數據庫層面解決這個問題很簡單,反相用了ORM EntityFramework之類的才不好解決數據庫解決方案
解決方案1:使用顯示事務

begin tran
select * from account with(rowlock updlock) where user_id = 123; --發布行級更新鎖,第二並發請求到這里嚴格排序,不管有多快,這里有個技巧,因為第二個並發撞進來第一句也是updlock所以兩個updlock之間會排斥
update account set balance...
commit

 


解決方案2:在代碼層使用分布事務

using (TransactionScope ts = new TransactionScope()) //用這個需要本地單獨開MSDTC (Distributed Transaction Coordinator)服務,並不一定通用 有門檻
{
exesql("update account set balance=balance where user_id =123"); //這一句很重要,事務中開頭一句update讓數據庫先發布一個x鎖,后面的並發將被嚴格排隊
exesql("select * from account where user_id = 123;");
exesql("update account set balance..");
ts.Complete();
}

 


方案3 ,在代碼入口使用線程鎖

public static object lockObj =new object();
public void Withdraw(int user_id,int amount){
  lock (lockObj){ //讓提現操作在線程線嚴格排序,不管並發有多快,缺點是不同的用戶 提現也得按順序排序,但一般提現操作是小概率操作,不會很密集,正常提現的沒阻塞感知,但是攻擊者可以反復發起請求,導致正確用戶提現變慢或阻塞
    exesql("select * from account where user_id = 123;");
    exesql("update account set balance..");

  }
}

 一個很重要的技巧是在一個事務內,第一句先 寫一個無意義的update  Account with(rowlock)   set  balance=balance where userid=123  ; 這個技巧在任何時候都適用,強制讓數據庫在事務期內發布x級獨占行級鎖鎖,后面的操作被嚴格排隊,就算攻擊者重復請求也只會阻塞他自己的用戶查詢,不會阻塞別人的


總結:最可行的是存儲過程方案1,缺點是不靈活,在如今ORM滿天飛的情況下新一代人很少有會寫存儲過程SQL了,
方案2,是一個折中方案,一般可控性還好
方案3,使用最簡單,基本是零成本,零難度,但是會有潛被拒絕服務攻擊的功能,但保證最重要的數據安全

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM