關於WEB金融系統中的提現安全問題很多人沒有深入思想,導致有漏洞,常常會遇到有些人遇到被攻擊到導資金損失的麻煩, 其實要徹底解決重覆併發請求 導致重覆提現問題,是需要花點心思的,並沒有看起來的那麼 簡單,即使是最直觀簡單的語句都是有漏洞的比如: 場景1 發現很多朋友的項目一個漏洞:先為一賬戶充值10 ...
關於WEB金融系統中的提現安全問題很多人沒有深入思想,導致有漏洞,常常會遇到有些人遇到被攻擊到導資金損失的麻煩, 其實要徹底解決重覆併發請求 導致重覆提現問題,是需要花點心思的,並沒有看起來的那麼 簡單,即使是最直觀簡單的語句都是有漏洞的比如:
-----------------------------------------場景1--------------------
發現很多朋友的項目一個漏洞:先為一賬戶充值100元,然後瞬間發送10次提現請求(都是提現100,提現介面是有做餘額不足校驗的),其中大約有四五次都是成功的,剩下的會報餘額不足。期望是,只有一次可以成功完成提現,分析到能部分請求能通過餘額不足校驗原因是,由於是瞬間發出的提現請求,這些請求中拿到的餘額數據都是餘額扣減之前的數據。
以上場景可以提煉出兩個關鍵步驟:
- 查詢餘額並校驗,select * from account where user_id = 123;
- 扣減餘額並支付,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,使用最簡單,基本是零成本,零難度,但是會有潛被拒絕服務攻擊的功能,但保證最重要的數據安全