1. 程式人生 > >由一次安全掃描引發的思考:如何保障 API 介面的安全性?

由一次安全掃描引發的思考:如何保障 API 介面的安全性?

![](https://cdn.geekdigging.com/technique-sharing/20200525/ssl.jpg) ## 引言 前段時間,公司對執行的系統進行了一次安全掃描,使用的工具是 IBM 公司提供的 AppScan 。 這個正所謂不掃不要緊,一掃嚇一跳,結果就掃出來這麼個問題。 我們的一個年老失修的內部系統,在登入的時候,被掃描出來安全隱患,具體學名是啥記不清了,大致就是我們在傳送登入請求的時候,有個欄位名是 `password` , AppScan 認為這個是不安全的,大概就是下面: ![](https://cdn.geekdigging.com/technique-sharing/20200525/login_demo.png) 我第一個反應是把這個欄位名字改一下,畢竟能簡單解決就簡單解決嘛,結果當然是啪啪啪打臉。 這個名字我不管是換成 `aaa` 還是 `bbb` ,再次掃描都還會報同樣的問題,唯一不同的地方就是安全報告上的欄位名換一換。 這個就有意思了,這個問題是來者不善啊,經過我一翻查詢(別問我怎麼查的,問就是瞎猜的),找到原因的所在了。 因為我們這個系統是一個內部系統,當時做登入這個人比較圖懶,就在頁面上簡單的做了個 form 表單提交,就比如這樣: ![](https://cdn.geekdigging.com/technique-sharing/20200525/html_pic.png) 這個程式碼我曾經在大學的大作業上這麼寫過,沒想到時隔多年我竟然又見到了這樣的程式碼,竟然讓我有一種老鄉見老鄉的特殊情感。 這個問題具體的原因是 AppScan 是直接檢測頁面上 `type='password'` 的輸入框,然後再檢查請求中是否有對應的欄位,別問我咋知道的,因為我幹過把這裡改成 `type='text'` 就不報錯了,唯一的缺點就是頁面上的密碼框將會明碼顯示密碼。 雖然我可以通過 js 來把輸入框裡的值動態的替換成任何我想要的樣子,比如點啊、星號啊以及一些其他的樣式,但是這麼幹總歸有點不道德。 問題找到了,那麼怎麼改呢? 到這裡就涉及到了我今天要聊的內容了,如何保障 API 介面的安全性? 首先這個問題我們分成兩個部分來看,客戶端和服務端。 ## 服務端 因為我本身是做服務端開發的,這個問題當然要從服務端聊起。 個人覺得安全措施主要體現在兩個方面,一個是如何保證資料在傳輸過程中的安全性,另一個是如何在資料已經到達服務端後,服務端如何識別資料,保證不被攻擊。 下面我們一條一條來聊: ### 1. HTTP 請求中的來源識別 HTTP 請求中的來源識別就是,服務端如何識別當前的請求是由自己的客戶端發起的,而不是由第三方模擬的請求。 我們先看下一個正常的 HTTP 請求的頭裡面會有什麼內容: ![](https://cdn.geekdigging.com/technique-sharing/20200525/request_head_demo.png) 我開啟百度的首頁,通過 network 隨便抓了一個請求,檢視這個請求的請求頭,這裡面我要說的幾個欄位都用紅框框起來了: * Origin:用於指明當前請求來自於哪個站點。 * Referer:用於指明當前的請求是從哪個頁面連結過來的 * User-Agent:用於標識當前的請求的瀏覽器或者系統的一些資訊。 我們一般會對 HTTP 請求頭中的 Origin 和 Referer 做白名單域名校驗,先判斷這個請求是不是由我們自己的域發出來的,然後再對 User-Agent 做一次校驗,用來保證當前的請求是由瀏覽器發出來的,而不是由什麼雜七雜八的模擬器發出來的。 當前,由於前端是完全不可被信任的,上面這幾個欄位都是可以被篡改和模擬的(我在前面寫爬蟲的文章中絕對寫過),但是,能做的校驗儘量做,我們不能一次把所有的漏洞都堵上,但是至少能堵上一部分。 ### 2. 資料加密 資料在傳輸過程中是很容易被抓包的,如果直接傳輸比如通過 http 協議,那麼使用者傳輸的資料可以被任何人獲取;所以必須對資料加密。 常見的做法對關鍵欄位加密比如使用者密碼直接通過 md5 加密;現在主流的做法是使用 https 協議,在 http 和 tcp 之間新增一層加密層( SSL 層),這一層負責資料的加密和解密。 ### 3. 資料簽名 增加簽名就是我們在傳送 HTTP 請求的時候,增加一個無法偽造的字串,用來保證資料在傳輸的過程中不被篡改。 資料簽名使用比較多的演算法是 MD5 演算法,這個演算法是將要提交的資料,通過某種方式組合成一個字串,然後通過 MD5 演算法生成一個簽名。 我用前面那個登入的介面舉個簡單的例子: ```shell srt:name={引數1}&password={引數2}&$key={使用者金鑰} MD5.encrypt(str) ``` 這裡的 key 是一個金鑰,由客戶端和服務端各持有一份,最終登入請求要提交的 json 資料就會是下面這個樣子: ```json { "name": "test", "password": "123", "sign": "098f6bcd4621d373cade4e832627b4f6" } ``` 金鑰是不參與資料提交,否則請求被劫持後,第三方就可以通過金鑰自己生成簽名,當然,如果覺得單純的 MD5 不夠安全的話,還可以在 MD5 的時候加鹽和加 hash ,進一步降低請求被劫持後存在模擬的風險。 ### 4. 時間戳 時間戳機制主要用來應對非法的 DDOS 攻擊,我們的請求經過的加密和簽名後,已經很難進行逆向破解了,但是有的攻擊者他在抓包後,並不在意裡面的具體資料,直接拿著抓的包進行攻擊,這就是臭名昭著的 DDOS 攻擊。 我們可以在引數中加上當前請求的時間戳,服務端拿到這個請求後會拿當前的時間和請求中的時間做比較,比如在 5 分鐘之內的才會流轉到後面的業務處理,在 5 分鐘以外的直接返回錯誤碼。 這裡要注意的是客戶端的時間和服務端的時間基本上是不可能一致的,加上請求本省在網路中傳輸還有耗時,所以時間限制的閥值不能設定的太小,防止合法的請求無法訪問。 我還是拿上面的登入舉例子,到這一步,我們的請求資料會變成下面這個樣子: ```json { "name": "test", "password": "123", "timestamp": 1590334946000, "sign": "098f6bcd4621d373cade4e832627b4f6" } ``` ### 5. AppID 很多時候,我們一個 API 介面可能並不是只會有一個客戶端進行呼叫,可能呼叫方會有非常多,我們的服務端為了驗證合法的呼叫使用者,可以新增一個 AppID 。 想要呼叫我們的 API 介面,必須通過線下的方式像我申請一個 AppID ,只有當這個 AppID 開通後,才能對我的介面進行合法的訪問,在進行介面訪問的時候,這個 AppID 需要新增到請求引數中,與其他資料一起提交。 到了這一步,我們上面那個登入介面的傳入引數就變成了下面這樣: ```json { "appid": "geekdigging", "name": "test", "password": "123", "timestamp": 1590334946, "sign": "098f6bcd4621d373cade4e832627b4f6" } ``` ### 6. 引數整體加密 我們上面對請求的引數進行了一系列的處理,總體思想是防止第三方進行抓包和破解,但是如果我不是第三方呢,比如我就在瀏覽器的 network 中進行抓包,這個請求中的資料我就能看的清清楚楚明明白白,攻擊者可以先通過正規的途徑進行訪問,當分析清楚我們的套路後再對請求進行偽造,開始攻擊,這時我們前面的努力好像就都白費了。 不要說什麼沒有人會這麼做,我就舉一個例子,支付寶網頁端的登入介面,如果能搞清楚其中請求的傳送規則,攻擊者就可以使用買來的使用者資料庫,進行批量撞庫測試,通過請求響應的結果就可以驗證一批的支付寶的賬號密碼(當然不會有這麼簡單哈,我只是舉例子)。 我們接下來還能再對請求做一次整體加密,現在主流的加密方式有**對稱加密**和**不對稱加密**兩種。 **對稱加密**:對稱金鑰在加密和解密的過程中使用的金鑰是相同的,常見的對稱加密演算法有 DES , AES , RC4 , Rabbit , TripleDes 等等。優點是計算速度快,缺點是在資料傳送前,傳送方和接收方必須商定好祕鑰,然後使雙方都能儲存好祕鑰,如果一方的祕鑰被洩露,那麼加密資訊也就不安全了。 **不對稱加密**:服務端會生成一對金鑰,私鑰存放在伺服器端,公鑰可以釋出給任何人使用。優點就是比起對稱加密更加安全,但是加解密的速度比對稱加密慢太多了。廣泛使用的是 RSA 演算法。 對上面我們提交的資料做一次 DES 加密,金鑰使用 123456 ,我們可以得到這樣一個結果: ```shell U2FsdGVkX18D+FiHsounFbttTFV8EToywxEHZcAEPkQpfwJqaMC5ssOZvf3JJQdB /b6M/zSJdAwNg6Jr8NGUGuaSyJrJx7G4KXlGBaIXIbkTn2RT2GL4NPrd8oPJDCMk y0yktsIWxVQP2hHbIckweEAdzRlcHvDn/0qa7zr0e1NfqY5IDDxWlSUKdwIbVC0o mIaD/dpTBm0= ``` 然後我們把這個字串放到請求中進行請求,我們剛才的登入請求就會變成這樣: ![](https://cdn.geekdigging.com/technique-sharing/20200525/login_demo_1.png) 相信這樣一來,超過 99% 的攻擊者看到 network 中這樣的抓包請求都會放棄,但是還剩下 1% 的人會開啟 Chrome 提供的開發者工具進行一行一行的 debug ,針對這部分人,我們下面在客戶端的段落裡再聊如何對付他們。 ### 7. 限流 很多時候,在某些併發比較高的場景下,基於對業務系統的保護,我們需要對請求訪問速率進行限制,防止訪問速率過高,把業務系統撐爆掉。 尤其是一些對外的介面,給客戶或者供應商使用的介面,因為呼叫方我們自己無法控制,天知道對方的程式碼會怎麼寫。 我曾經見過供應商把我們提供的修改資料的介面拿來當做批量介面跑批,每天晚上都能把那個服務跑掛掉,後來直到我們去問,供應商他們才說每天晚上會用這個介面做上千萬的資料同步,我也是醉了。 出於安全的角度考慮,在服務端做限流就顯得十分有必要。 服務端限流的演算法常見的有這麼幾種:令牌桶限流、漏桶限流、計數器限流。 **令牌桶限流**:令牌桶演算法的原理是系統以一定速率向桶中放入令牌,填滿了就丟棄令牌;請求來時會先從桶中取出令牌,如果能取到令牌,則可以繼續完成請求,否則等待或者拒絕服務;令牌桶允許一定程度突發流量,只要有令牌就可以處理,支援一次拿多個令牌。 **漏桶限流**:漏桶演算法的原理是按照固定常量速率流出請求,流入請求速率任意,當請求數超過桶的容量時,新的請求等待或者拒絕服務;可以看出漏桶演算法可以強制限制資料的傳輸速度。 **計數器限流**:計數器是一種比較簡單粗暴的演算法,主要用來限制總併發數,比如資料庫連線池、執行緒池、秒殺的併發數;計數器限流只要一定時間內的總請求數超過設定的閥值則進行限流。 實現方面來講, Guava 提供了 RateLimiter 工具類是基於基於令牌桶演算法,有需要的同學可以自己度娘一下。 ### 8. 黑名單 黑名單機制已經有點風控的概念了,我們可以對非法操作進行定義。 比如記錄每個 AppID 的訪問頻次,如果在 30 分鐘內,發生了 5 次或者以上的超頻訪問並且超出了 10 倍以上的訪問量,這時可以將這個 AppID 放入黑名單中, 24 小時以後或者呼叫方線下聯絡才能將這個 AppID 取出。 再比如記錄 AppID 超時訪問次數,正常來講,超時訪問不會頻繁發生,如果在某個時間段內,大量的出現超時訪問,這個 AppID 一定存在問題,也可以將其先放入黑名單中讓它冷靜冷靜。 黑名單實際上更多的是應用在業務層面,比如大家可能碰到過的拼爹爹的風控,直接把賬戶扔到黑名單裡面,禁止這個賬戶對某些補貼商品的下單。 ## 客戶端 在當今的網際網路時代,網頁和 APP 成為了主流的資訊載體。 其中 APP 是可以使用一些加固技術對 APP 進行加固,防止別人進行暴力破解。 而網頁就比較困難了,網頁的動態都是依靠 JavaScript 來完成的,邏輯是依賴於 JavaScript 來實現的,而 JavaScript 又有下面的特點: * JavaScript 程式碼運行於客戶端,也就是它必須要在使用者瀏覽器端載入並執行。 * JavaScript 程式碼是公開透明的,也就是說瀏覽器可以直接獲取到正在執行的 JavaScript 的原始碼。 基於這兩點,導致了 JavaScript 程式碼是不安全的,任何人都可以讀取、分析、盜用、篡改 JavaScript 程式碼。 所以說, JavaScript 如果不進行一些處理,不管使用瞭如何高超的加解密方案,在被人找到其中的邏輯後,被模擬或者複製將變得在所難免。 前端 JavaScript 常見的加固方案有這麼幾種:壓縮、混淆、加密 。 ### 1. 壓縮 程式碼壓縮,就是去除 JavaScript 程式碼中不必要的空格、換行等內容,把一些可能公用的程式碼進行處理實現共享,最後輸出的結果都壓縮為一行或者幾行內容,程式碼可讀性變得很差,同時也能提高網站載入速度,就想下面這樣: ![](https://cdn.geekdigging.com/technique-sharing/20200525/min_js_demo.png) 這個是我從百度的頁面上隨便找了一個 js 截出來。 如果是單純從去除空行空格這個角度上來對程式碼進行壓縮,其實幾乎是沒有任何防護作用的,因為這種壓縮方式僅僅是降低了程式碼的直接可讀性。 我們可以通過各種工具對程式碼進行格式化,包括 Chrome 瀏覽器本身就提供了這個功能。 目前主流的前端技術都會使用 Webpack 進行打包,Webpack 會對原始碼進行編譯和壓縮,輸出幾個打包好的 JavaScript 檔案,其中我們可以看到輸出的 JavaScript 檔名帶有一些不規則字串,同時檔案內容可能只有幾行內容,變數名都是一些簡單字母表示。 這其中就包含 JavaScript 壓縮技術,比如一些公共的庫輸出成 bundle 檔案,一些呼叫邏輯壓縮和轉義成幾行程式碼,這些都屬於 JavaScript 壓縮。另外其中也包含了一些很基礎的 JavaScript 混淆技術,比如把變數名、方法名替換成一些簡單字元,降低程式碼可讀性。 整體上來講, JavaScript 壓縮術只能在很小的程度上起到防護作用,要想真正提高防護效果還得依靠 JavaScript 混淆和加密技術。 ### 2. 混淆 JavaScript 混淆是完全是在 JavaScript 上面進行的處理,它的目的就是使得 JavaScript 變得難以閱讀和分析,大大降低程式碼可讀性,是一種很實用的 JavaScript 保護方案。 JavaScript 混淆器大致有兩種: * 通過正則替換實現的混淆器 * 通過語法樹替換實現的混淆器 第一種實現成本低,但是效果也一般,適合對混淆要求不高的場景。第二種實現成本較高,但是更靈活,而且更安全,更適合對抗場景。 通過語法樹替換實現的混淆器,這種混淆方式的實現有點複雜了,我這裡就不展開去聊了,有興趣的同學可以參考這篇文章:https://www.zhihu.com/question/47047191/answer/121013968 。 針對修改語法樹進行混淆的方式,目前有一家做的比較好並且提供商業服務的是 jscrambler ,他們的官網地址:https://jscrambler.com/ 。 總之,以上方案都是 JavaScript 混淆的實現方式,可以在不同程度上保護 JavaScript 程式碼。 在一般的場景中,第一種混淆方式足夠我們使用,現在 JavaScript 混淆主流的實現是 javascript-obfuscator 這個庫,利用它我們可以非常方便地實現頁面的混淆,它與 Webpack 結合起來,最終可以輸出壓縮和混淆後的 JavaScript 程式碼,使得可讀性大大降低,難以逆向。 ### 3. 加密 不同於 JavaScript 混淆技術,JavaScript 加密技術可以說是對 JavaScript 混淆技術防護的進一步升級,其基本思路是將一些核心邏輯使用諸如 C/C++ 語言來編寫,並通過 JavaScript 呼叫執行,從而起到二進位制級別的防護作用。 其加密的方式現在有 Emscripten 和 WebAssembly 等,其中後者越來越成為主流。 感興趣的同學可以自行度娘瞭解下。 ## 小結 上面介紹了這麼多,只是為了我們的程式能夠更加安全穩定的執行,減少因為攻擊而產生的損失(加班)。 很多內容都是來自度娘後進行資訊整理,希望各位能夠善用搜索引擎這個工具。 ## 參考 https://my.oschina.net/OutOfMemory/blog/3131916 https://mp.weixin.qq.com/s/NAZv4OjZjqgz8L