由一次安全掃描引發的思考:如何保障 api 接口的安全性?

引言

前段時間,公司對運行的系統進行了一次安全掃描,使用的工具是 IBM 公司提供的 AppScan 。

這個正所謂不掃不要緊,一掃嚇一跳,結果就掃出來這麼個問題。

我們的一個年老失修的內部系統,在登錄的時候,被掃描出來安全隱患,具體學名是啥記不清了,大致就是我們在發送登錄請求的時候,有個字段名是 password , AppScan 認爲這個是不安全的,大概就是下面:

6cca27ac89d743f8ac904ff5d2b7ae5b

我第一個反應是把這個字段名字改一下,畢竟能簡單解決就簡單解決嘛,結果當然是啪啪啪打臉。

這個名字我不管是換成 aaa 還是 bbb ,再次掃描都還會報同樣的問題,唯一不同的地方就是安全報告上的字段名換一換。

這個就有意思了,這個問題是來者不善啊,經過我一翻查找(別問我怎麼查的,問就是瞎猜的),找到原因的所在了。

因爲我們這個系統是一個內部系統,當時做登錄這個人比較圖懶,就在頁面上簡單的做了個 form 表單提交,就比如這樣:

e7cc26e543f94cb180e449aa6372f649

這個代碼我曾經在大學的大作業上這麼寫過,沒想到時隔多年我竟然又見到了這樣的代碼,竟然讓我有一種老鄉見老鄉的特殊情感。

這個問題具體的原因是 AppScan 是直接檢測頁面上 type='password' 的輸入框,然後再檢查請求中是否有對應的字段,別問我咋知道的,因爲我幹過把這裏改成 type='text' 就不報錯了,唯一的缺點就是頁面上的密碼框將會明碼顯示密碼。

雖然我可以通過 js 來把輸入框裏的值動態的替換成任何我想要的樣子,比如點啊、星號啊以及一些其他的樣式,但是這麼幹總歸有點不道德。

問題找到了,那麼怎麼改呢?

到這裏就涉及到了我今天要聊的內容了,如何保障 API 接口的安全性?

首先這個問題我們分成兩個部分來看,客戶端和服務端。

服務端

因爲我本身是做服務端開發的,這個問題當然要從服務端聊起。

個人覺得安全措施主要體現在兩個方面,一個是如何保證數據在傳輸過程中的安全性,另一個是如何在數據已經到達服務端後,服務端如何識別數據,保證不被***。

下面我們一條一條來聊:

1. HTTP 請求中的來源識別

HTTP 請求中的來源識別就是,服務端如何識別當前的請求是由自己的客戶端發起的,而不是由第三方模擬的請求。

我們先看下一個正常的 HTTP 請求的頭裏面會有什麼內容:

b018b864382b4d0bb74c5b34bbf9dc8c

我打開百度的首頁,通過 network 隨便抓了一個請求,查看這個請求的請求頭,這裏面我要說的幾個字段都用紅框框起來了:

  • Origin:用於指明當前請求來自於哪個站點。

  • Referer:用於指明當前的請求是從哪個頁面鏈接過來的

  • User-Agent:用於標識當前的請求的瀏覽器或者系統的一些信息。

我們一般會對 HTTP 請求頭中的 Origin 和 Referer 做白名單域名校驗,先判斷這個請求是不是由我們自己的域發出來的,然後再對 User-Agent 做一次校驗,用來保證當前的請求是由瀏覽器發出來的,而不是由什麼雜七雜八的模擬器發出來的。

當前,由於前端是完全不可被信任的,上面這幾個字段都是可以被篡改和模擬的(我在前面寫爬蟲的文章中絕對寫過),但是,能做的校驗儘量做,我們不能一次把所有的漏洞都堵上,但是至少能堵上一部分。

2. 數據加密

數據在傳輸過程中是很容易被抓包的,如果直接傳輸比如通過 http 協議,那麼用戶傳輸的數據可以被任何人獲取;所以必須對數據加密。

常見的做法對關鍵字段加密比如用戶密碼直接通過 md5 加密;現在主流的做法是使用 https 協議,在 http 和 tcp 之間添加一層加密層( SSL 層),這一層負責數據的加密和解密。

3. 數據簽名

增加簽名就是我們在發送 HTTP 請求的時候,增加一個無法僞造的字符串,用來保證數據在傳輸的過程中不被篡改。

數據簽名使用比較多的算法是 MD5 算法,這個算法是將要提交的數據,通過某種方式組合成一個字符串,然後通過 MD5 算法生成一個簽名。

我用前面那個登錄的接口舉個簡單的例子:

srt:name={參數1}&password={參數2}&$key={用戶密鑰}
MD5.encrypt(str)

這裏的 key 是一個密鑰,由客戶端和服務端各持有一份,最終登錄請求要提交的 json 數據就會是下面這個樣子:

{
	"name": "test",
	"password": "123",
	"sign": "098f6bcd4621d373cade4e832627b4f6"
}

密鑰是不參與數據提交,否則請求被劫持後,第三方就可以通過密鑰自己生成簽名,當然,如果覺得單純的 MD5 不夠安全的話,還可以在 MD5 的時候加鹽和加 hash ,進一步降低請求被劫持後存在模擬的風險。

4. 時間戳

時間戳機制主要用來應對非法的 DDOS ***,我們的請求經過的加密和簽名後,已經很難進行逆向破解了,但是有的***者他在抓包後,並不在意裏面的具體數據,直接拿着抓的包進行***,這就是臭名昭著的 DDOS ***。

我們可以在參數中加上當前請求的時間戳,服務端拿到這個請求後會拿當前的時間和請求中的時間做比較,比如在 5 分鐘之內的纔會流轉到後面的業務處理,在 5 分鐘以外的直接返回錯誤碼。

這裏要注意的是客戶端的時間和服務端的時間基本上是不可能一致的,加上請求本省在網絡中傳輸還有耗時,所以時間限制的閥值不能設定的太小,防止合法的請求無法訪問。

我還是拿上面的登錄舉例子,到這一步,我們的請求數據會變成下面這個樣子:

{
	"name": "test",
	"password": "123",
	"timestamp": 1590334946000,
	"sign": "098f6bcd4621d373cade4e832627b4f6"
}

5. AppID

很多時候,我們一個 API 接口可能並不是只會有一個客戶端進行調用,可能調用方會有非常多,我們的服務端爲了驗證合法的調用用戶,可以添加一個 AppID 。

想要調用我們的 API 接口,必須通過線下的方式像我申請一個 AppID ,只有當這個 AppID 開通後,才能對我的接口進行合法的訪問,在進行接口訪問的時候,這個 AppID 需要添加到請求參數中,與其他數據一起提交。

到了這一步,我們上面那個登錄接口的傳入參數就變成了下面這樣:

{
	"appid": "geekdigging",
	"name": "test",
	"password": "123",
	"timestamp": 1590334946,
	"sign": "098f6bcd4621d373cade4e832627b4f6"
}

6. 參數整體加密

我們上面對請求的參數進行了一系列的處理,總體思想是防止第三方進行抓包和破解,但是如果我不是第三方呢,比如我就在瀏覽器的 network 中進行抓包,這個請求中的數據我就能看的清清楚楚明明白白,***者可以先通過正規的途徑進行訪問,當分析清楚我們的套路後再對請求進行僞造,開始***,這時我們前面的努力好像就都白費了。

不要說什麼沒有人會這麼做,我就舉一個例子,支付寶網頁端的登錄接口,如果能搞清楚其中請求的發送規則,***者就可以使用買來的用戶數據庫,進行批量撞庫測試,通過請求響應的結果就可以驗證一批的支付寶的賬號密碼(當然不會有這麼簡單哈,我只是舉例子)。

我們接下來還能再對請求做一次整體加密,現在主流的加密方式有對稱加密不對稱加密兩種。

對稱加密:對稱密鑰在加密和解密的過程中使用的密鑰是相同的,常見的對稱加密算法有 DES , AES , RC4 , Rabbit , TripleDes 等等。優點是計算速度快,缺點是在數據傳送前,發送方和接收方必須商定好祕鑰,然後使雙方都能保存好祕鑰,如果一方的祕鑰被泄露,那麼加密信息也就不安全了。

不對稱加密:服務端會生成一對密鑰,私鑰存放在服務器端,公鑰可以發佈給任何人使用。優點就是比起對稱加密更加安全,但是加解密的速度比對稱加密慢太多了。廣泛使用的是 RSA 算法。

對上面我們提交的數據做一次 DES 加密,密鑰使用 123456 ,我們可以得到這樣一個結果:

U2FsdGVkX18D+FiHsounFbttTFV8EToywxEHZcAEPkQpfwJqaMC5ssOZvf3JJQdB
/b6M/zSJdAwNg6Jr8NGUGuaSyJrJx7G4KXlGBaIXIbkTn2RT2GL4NPrd8oPJDCMk
y0yktsIWxVQP2hHbIckweEAdzRlcHvDn/0qa7zr0e1NfqY5IDDxWlSUKdwIbVC0o
mIaD/dpTBm0=

然後我們把這個字符串放到請求中進行請求,我們剛纔的登錄請求就會變成這樣:

78cf54936a0e422692e4707291379642

相信這樣一來,超過 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 代碼中不必要的空格、換行等內容,把一些可能公用的代碼進行處理實現共享,最後輸出的結果都壓縮爲一行或者幾行內容,代碼可讀性變得很差,同時也能提高網站加載速度,就想下面這樣:

24049ab037264fbaa982fee3f7ee4bbf

這個是我從百度的頁面上隨便找了一個 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 等,其中後者越來越成爲主流。

感興趣的同學可以自行度娘瞭解下。

小結

上面介紹了這麼多,只是爲了我們的程序能夠更加安全穩定的運行,減少因爲***而產生的損失(加班)。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章