引言
前段時間苫耸,公司對運行的系統(tǒng)進行了一次安全掃描州邢,使用的工具是 IBM 公司提供的 AppScan 。
這個正所謂不掃不要緊褪子,一掃嚇一跳量淌,結果就掃出來這么個問題。
我們的一個年老失修的內(nèi)部系統(tǒng)嫌褪,在登錄的時候呀枢,被掃描出來安全隱患,具體學名是啥記不清了笼痛,大致就是我們在發(fā)送登錄請求的時候裙秋,有個字段名是password, AppScan 認為這個是不安全的缨伊,大概就是下面:
我第一個反應是把這個字段名字改一下摘刑,畢竟能簡單解決就簡單解決嘛,結果當然是啪啪啪打臉倘核。
這個名字我不管是換成aaa還是bbb泣侮,再次掃描都還會報同樣的問題,唯一不同的地方就是安全報告上的字段名換一換紧唱。
這個就有意思了活尊,這個問題是來者不善啊隶校,經(jīng)過我一翻查找(別問我怎么查的,問就是瞎猜的)蛹锰,找到原因的所在了深胳。
因為我們這個系統(tǒng)是一個內(nèi)部系統(tǒng),當時做登錄這個人比較圖懶铜犬,就在頁面上簡單的做了個 form 表單提交舞终,就比如這樣:
這個代碼我曾經(jīng)在大學的大作業(yè)上這么寫過,沒想到時隔多年我竟然又見到了這樣的代碼癣猾,竟然讓我有一種老鄉(xiāng)見老鄉(xiāng)的特殊情感敛劝。
這個問題具體的原因是 AppScan 是直接檢測頁面上type='password'的輸入框,然后再檢查請求中是否有對應的字段纷宇,別問我咋知道的夸盟,因為我干過把這里改成type='text'就不報錯了,唯一的缺點就是頁面上的密碼框?qū)鞔a顯示密碼像捶。
雖然我可以通過 js 來把輸入框里的值動態(tài)的替換成任何我想要的樣子上陕,比如點啊、星號啊以及一些其他的樣式拓春,但是這么干總歸有點不道德释簿。
問題找到了,那么怎么改呢硼莽?
到這里就涉及到了我今天要聊的內(nèi)容了庶溶,如何保障 API 接口的安全性?
首先這個問題我們分成兩個部分來看懂鸵,客戶端和服務端渐尿。
服務端
因為我本身是做服務端開發(fā)的,這個問題當然要從服務端聊起矾瑰。
個人覺得安全措施主要體現(xiàn)在兩個方面,一個是如何保證數(shù)據(jù)在傳輸過程中的安全性隘擎,另一個是如何在數(shù)據(jù)已經(jīng)到達服務端后殴穴,服務端如何識別數(shù)據(jù),保證不被攻擊货葬。
下面我們一條一條來聊:
1. HTTP 請求中的來源識別
HTTP 請求中的來源識別就是采幌,服務端如何識別當前的請求是由自己的客戶端發(fā)起的,而不是由第三方模擬的請求震桶。
我們先看下一個正常的 HTTP 請求的頭里面會有什么內(nèi)容:
我打開百度的首頁休傍,通過 network 隨便抓了一個請求,查看這個請求的請求頭蹲姐,這里面我要說的幾個字段都用紅框框起來了:
- Origin:用于指明當前請求來自于哪個站點磨取。
- Referer:用于指明當前的請求是從哪個頁面鏈接過來的
- User-Agent:用于標識當前的請求的瀏覽器或者系統(tǒng)的一些信息人柿。我們一般會對 HTTP 請求頭中的 Origin 和 Referer 做白名單域名校驗,先判斷這個請求是不是由我們自己的域發(fā)出來的忙厌,然后再對 User-Agent 做一次校驗凫岖,用來保證當前的請求是由瀏覽器發(fā)出來的,而不是由什么雜七雜八的模擬器發(fā)出來的逢净。
當前哥放,由于前端是完全不可被信任的,上面這幾個字段都是可以被篡改和模擬的(我在前面寫爬蟲的文章中絕對寫過)爹土,但是甥雕,能做的校驗盡量做,我們不能一次把所有的漏洞都堵上胀茵,但是至少能堵上一部分社露。
2. 數(shù)據(jù)加密
數(shù)據(jù)在傳輸過程中是很容易被抓包的,如果直接傳輸比如通過 http 協(xié)議宰掉,那么用戶傳輸?shù)臄?shù)據(jù)可以被任何人獲群巧凇;所以必須對數(shù)據(jù)加密轨奄。
常見的做法對關鍵字段加密比如用戶密碼直接通過 md5 加密孟害;現(xiàn)在主流的做法是使用 https 協(xié)議,在 http 和 tcp 之間添加一層加密層( SSL 層)挪拟,這一層負責數(shù)據(jù)的加密和解密挨务。
3. 數(shù)據(jù)簽名
增加簽名就是我們在發(fā)送 HTTP 請求的時候,增加一個無法偽造的字符串玉组,用來保證數(shù)據(jù)在傳輸?shù)倪^程中不被篡改谎柄。
數(shù)據(jù)簽名使用比較多的算法是 MD5 算法,這個算法是將要提交的數(shù)據(jù)朝巫,通過某種方式組合成一個字符串石景,然后通過 MD5 算法生成一個簽名。
我用前面那個登錄的接口舉個簡單的例子:
srt:name={參數(shù)1}&password={參數(shù)2}&$key={用戶密鑰} MD5.encrypt(str)
這里的 key 是一個密鑰潮孽,由客戶端和服務端各持有一份揪荣,最終登錄請求要提交的 json 數(shù)據(jù)就會是下面這個樣子:
{ "name": "test", "password": "123", "sign": "098f6bcd4621d373cade4e832627b4f6"}
密鑰是不參與數(shù)據(jù)提交往史,否則請求被劫持后,第三方就可以通過密鑰自己生成簽名椎例,當然挨决,如果覺得單純的 MD5 不夠安全的話请祖,還可以在 MD5 的時候加鹽和加 hash 凰棉,進一步降低請求被劫持后存在模擬的風險。
4. 時間戳
時間戳機制主要用來應對非法的 DDOS 攻擊撒犀,我們的請求經(jīng)過的加密和簽名后或舞,已經(jīng)很難進行逆向破解了,但是有的攻擊者他在抓包后映凳,并不在意里面的具體數(shù)據(jù),直接拿著抓的包進行攻擊仆救,這就是臭名昭著的 DDOS 攻擊矫渔。
我們可以在參數(shù)中加上當前請求的時間戳,服務端拿到這個請求后會拿當前的時間和請求中的時間做比較顿痪,比如在 5 分鐘之內(nèi)的才會流轉(zhuǎn)到后面的業(yè)務處理油够,在 5 分鐘以外的直接返回錯誤碼石咬。
這里要注意的是客戶端的時間和服務端的時間基本上是不可能一致的,加上請求本省在網(wǎng)絡中傳輸還有耗時鬼悠,所以時間限制的閥值不能設定的太小,防止合法的請求無法訪問。
我還是拿上面的登錄舉例子袜啃,到這一步幸缕,我們的請求數(shù)據(jù)會變成下面這個樣子:
{ "name": "test", "password": "123", "timestamp": 1590334946000, "sign": "098f6bcd4621d373cade4e832627b4f6" }
5. AppID
很多時候晰韵,我們一個 API 接口可能并不是只會有一個客戶端進行調(diào)用雪猪,可能調(diào)用方會有非常多起愈,我們的服務端為了驗證合法的調(diào)用用戶,可以添加一個 AppID 官觅。
想要調(diào)用我們的 API 接口阐污,必須通過線下的方式像我申請一個 AppID ,只有當這個 AppID 開通后功氨,才能對我的接口進行合法的訪問手幢,在進行接口訪問的時候,這個 AppID 需要添加到請求參數(shù)中纵势,與其他數(shù)據(jù)一起提交管钳。
到了這一步,我們上面那個登錄接口的傳入?yún)?shù)就變成了下面這樣:
{ "appid": "geekdigging", "name": "test", "password": "123", "timestamp": 1590334946, "sign": "098f6bcd4621d373cade4e832627b4f6" }
6. 參數(shù)整體加密
我們上面對請求的參數(shù)進行了一系列的處理牛曹,總體思想是防止第三方進行抓包和破解黎比,但是如果我不是第三方呢鸳玩,比如我就在瀏覽器的 network 中進行抓包,這個請求中的數(shù)據(jù)我就能看的清清楚楚明明白白颓帝,攻擊者可以先通過正規(guī)的途徑進行訪問,當分析清楚我們的套路后再對請求進行偽造吕座,開始攻擊瘪板,這時我們前面的努力好像就都白費了。
不要說什么沒有人會這么做锣枝,我就舉一個例子魏身,支付寶網(wǎng)頁端的登錄接口,如果能搞清楚其中請求的發(fā)送規(guī)則税朴,攻擊者就可以使用買來的用戶數(shù)據(jù)庫家制,進行批量撞庫測試,通過請求響應的結果就可以驗證一批的支付寶的賬號密碼(當然不會有這么簡單哈觅廓,我只是舉例子)涵但。
我們接下來還能再對請求做一次整體加密矮瘟,現(xiàn)在主流的加密方式有對稱加密和不對稱加密兩種。
對稱加密:對稱密鑰在加密和解密的過程中使用的密鑰是相同的劫侧,常見的對稱加密算法有 DES 哨啃, AES , RC4 审姓, Rabbit 祝峻, TripleDes 等等次坡。優(yōu)點是計算速度快,缺點是在數(shù)據(jù)傳送前宋距,發(fā)送方和接收方必須商定好秘鑰谚赎,然后使雙方都能保存好秘鑰,如果一方的秘鑰被泄露雳灵,那么加密信息也就不安全了闸盔。
不對稱加密:服務端會生成一對密鑰,私鑰存放在服務器端躲撰,公鑰可以發(fā)布給任何人使用击费。優(yōu)點就是比起對稱加密更加安全,但是加解密的速度比對稱加密慢太多了谆棱。廣泛使用的是 RSA 算法圆仔。
對上面我們提交的數(shù)據(jù)做一次 DES 加密荧缘,密鑰使用 123456 ,我們可以得到這樣一個結果:
U2FsdGVkX18D+FiHsounFbttTFV8EToywxEHZcAEPkQpfwJqaMC5ssOZvf3JJQdB/b6M/zSJdAwNg6Jr8NGUGuaSyJrJx7G4KXlGBaIXIbkTn2RT2GL4NPrd8oPJDCMky0yktsIWxVQP2hHbIckweEAdzRlcHvDn/0qa7zr0e1NfqY5IDDxWlSUKdwIbVC0o mIaD/dpTBm0=
然后我們把這個字符串放到請求中進行請求信姓,我們剛才的登錄請求就會變成這樣:
相信這樣一來绸罗,超過 99% 的攻擊者看到 network 中這樣的抓包請求都會放棄珊蟀,但是還剩下 1% 的人會打開 Chrome 提供的開發(fā)者工具進行一行一行的 debug 外驱,針對這部分人腻窒,我們下面在客戶端的段落里再聊如何對付他們儿子。
7. 限流
很多時候,在某些并發(fā)比較高的場景下蒋譬,基于對業(yè)務系統(tǒng)的保護愉适,我們需要對請求訪問速率進行限制,防止訪問速率過高剂买,把業(yè)務系統(tǒng)撐爆掉腰湾。
尤其是一些對外的接口,給客戶或者供應商使用的接口倒槐,因為調(diào)用方我們自己無法控制讨越,天知道對方的代碼會怎么寫永毅。
我曾經(jīng)見過供應商把我們提供的修改數(shù)據(jù)的接口拿來當做批量接口跑批,每天晚上都能把那個服務跑掛掉着逐,后來直到我們?nèi)栆庵趟麄儾耪f每天晚上會用這個接口做上千萬的數(shù)據(jù)同步,我也是醉了秀姐。
出于安全的角度考慮若贮,在服務端做限流就顯得十分有必要。
服務端限流的算法常見的有這么幾種:令牌桶限流蠢沿、漏桶限流舷蟀、計數(shù)器限流。
令牌桶限流:令牌桶算法的原理是系統(tǒng)以一定速率向桶中放入令牌,填滿了就丟棄令牌精绎;請求來時會先從桶中取出令牌代乃,如果能取到令牌,則可以繼續(xù)完成請求原茅,否則等待或者拒絕服務堕仔;令牌桶允許一定程度突發(fā)流量,只要有令牌就可以處理通贞,支持一次拿多個令牌昌罩。
漏桶限流:漏桶算法的原理是按照固定常量速率流出請求灾馒,流入請求速率任意,當請求數(shù)超過桶的容量時轨功,新的請求等待或者拒絕服務傅物;可以看出漏桶算法可以強制限制數(shù)據(jù)的傳輸速度董饰。
計數(shù)器限流:計數(shù)器是一種比較簡單粗暴的算法圆米,主要用來限制總并發(fā)數(shù)啄栓,比如數(shù)據(jù)庫連接池昙楚、線程池、秒殺的并發(fā)數(shù)削葱;計數(shù)器限流只要一定時間內(nèi)的總請求數(shù)超過設定的閥值則進行限流淳梦。
實現(xiàn)方面來講爆袍, Guava 提供了 RateLimiter 工具類是基于基于令牌桶算法,有需要的同學可以自己度娘一下弦疮。
8. 黑名單
黑名單機制已經(jīng)有點風控的概念了蜘醋,我們可以對非法操作進行定義。
比如記錄每個 AppID 的訪問頻次闲先,如果在 30 分鐘內(nèi)伺糠,發(fā)生了 5 次或者以上的超頻訪問并且超出了 10 倍以上的訪問量斥季,這時可以將這個 AppID 放入黑名單中, 24 小時以后或者調(diào)用方線下聯(lián)系才能將這個 AppID 取出舵揭。
再比如記錄 AppID 超時訪問次數(shù)躁锡,正常來講映之,超時訪問不會頻繁發(fā)生蜡坊,如果在某個時間段內(nèi)秕衙,大量的出現(xiàn)超時訪問僵刮,這個 AppID 一定存在問題,也可以將其先放入黑名單中讓它冷靜冷靜勇吊。
黑名單實際上更多的是應用在業(yè)務層面窍仰,比如大家可能碰到過的拼爹爹的風控辈赋,直接把賬戶扔到黑名單里面膏燕,禁止這個賬戶對某些補貼商品的下單坝辫。
客戶端
在當今的互聯(lián)網(wǎng)時代,網(wǎng)頁和 APP 成為了主流的信息載體竭业。
其中 APP 是可以使用一些加固技術對 APP 進行加固及舍,防止別人進行暴力破解锯玛。
而網(wǎng)頁就比較困難了,網(wǎng)頁的動態(tài)都是依靠 JavaScript 來完成的拙友,邏輯是依賴于 JavaScript 來實現(xiàn)的歼郭,而 JavaScript 又有下面的特點:
JavaScript 代碼運行于客戶端病曾,也就是它必須要在用戶瀏覽器端加載并運行漾根。JavaScript 代碼是公開透明的捷兰,也就是說瀏覽器可以直接獲取到正在運行的 JavaScript 的源碼贡茅。基于這兩點赁还,導致了 JavaScript 代碼是不安全的艘策,任何人都可以讀取渊季、分析、盜用驯妄、篡改 JavaScript 代碼合砂。
所以說翩伪, JavaScript 如果不進行一些處理,不管使用了如何高超的加解密方案凛剥,在被人找到其中的邏輯后轻姿,被模擬或者復制將變得在所難免踢代。
前端 JavaScript 常見的加固方案有這么幾種:壓縮胳挎、混淆、加密 窑眯。
1. 壓縮
代碼壓縮,就是去除 JavaScript 代碼中不必要的空格炊林、換行等內(nèi)容卷要,把一些可能公用的代碼進行處理實現(xiàn)共享僧叉,最后輸出的結果都壓縮為一行或者幾行內(nèi)容菇怀,代碼可讀性變得很差,同時也能提高網(wǎng)站加載速度郎笆,就想下面這樣:
這個是我從百度的頁面上隨便找了一個 js 截出來宛蚓。
如果是單純從去除空行空格這個角度上來對代碼進行壓縮,其實幾乎是沒有任何防護作用的,因為這種壓縮方式僅僅是降低了代碼的直接可讀性竞思。
我們可以通過各種工具對代碼進行格式化钞护,包括 Chrome 瀏覽器本身就提供了這個功能难咕。
目前主流的前端技術都會使用 Webpack 進行打包,Webpack 會對源代碼進行編譯和壓縮暮刃,輸出幾個打包好的 JavaScript 文件椭懊,其中我們可以看到輸出的 JavaScript 文件名帶有一些不規(guī)則字符串步势,同時文件內(nèi)容可能只有幾行內(nèi)容,變量名都是一些簡單字母表示漠魏。
這其中就包含 JavaScript 壓縮技術妄均,比如一些公共的庫輸出成 bundle 文件,一些調(diào)用邏輯壓縮和轉(zhuǎn)義成幾行代碼奕纫,這些都屬于 JavaScript 壓縮匹层。另外其中也包含了一些很基礎的 JavaScript 混淆技術锌蓄,比如把變量名瘸爽、方法名替換成一些簡單字符,降低代碼可讀性灵汪。
整體上來講享言, JavaScript 壓縮術只能在很小的程度上起到防護作用渗鬼,要想真正提高防護效果還得依靠 JavaScript 混淆和加密技術譬胎。
2. 混淆
JavaScript 混淆是完全是在 JavaScript 上面進行的處理,它的目的就是使得 JavaScript 變得難以閱讀和分析偏化,大大降低代碼可讀性镐侯,是一種很實用的 JavaScript 保護方案。
JavaScript 混淆器大致有兩種:
通過正則替換實現(xiàn)的混淆器通過語法樹替換實現(xiàn)的混淆器第一種實現(xiàn)成本低只怎,但是效果也一般怜俐,適合對混淆要求不高的場景拍鲤。第二種實現(xiàn)成本較高,但是更靈活擅这,而且更安全景鼠,更適合對抗場景仲翎。
通過語法樹替換實現(xiàn)的混淆器,這種混淆方式的實現(xiàn)有點復雜了铛漓,我這里就不展開去聊了溯香。
針對修改語法樹進行混淆的方式,目前有一家做的比較好并且提供商業(yè)服務的是 jscrambler浓恶。
總之玫坛,以上方案都是 JavaScript 混淆的實現(xiàn)方式,可以在不同程度上保護 JavaScript 代碼包晰。
在一般的場景中,第一種混淆方式足夠我們使用伐憾,現(xiàn)在 JavaScript 混淆主流的實現(xiàn)是 javascript-obfuscator 這個庫勉痴,利用它我們可以非常方便地實現(xiàn)頁面的混淆,它與 Webpack 結合起來塞耕,最終可以輸出壓縮和混淆后的 JavaScript 代碼,使得可讀性大大降低嘴瓤,難以逆向扫外。
3. 加密
不同于 JavaScript 混淆技術,JavaScript 加密技術可以說是對 JavaScript 混淆技術防護的進一步升級廓脆,其基本思路是將一些核心邏輯使用諸如 C/C++ 語言來編寫筛谚,并通過 JavaScript 調(diào)用執(zhí)行,從而起到二進制級別的防護作用停忿。
其加密的方式現(xiàn)在有 Emscripten 和 WebAssembly 等驾讲,其中后者越來越成為主流。
感興趣的同學可以自行度娘了解下。
小結
上面介紹了這么多吮铭,只是為了我們的程序能夠更加安全穩(wěn)定的運行时迫,減少因為攻擊而產(chǎn)生的損失(加班)。
很多內(nèi)容都是來自度娘后進行信息整理谓晌,希望各位能夠善用搜索引擎這個工具掠拳。