[架構師觀點] 資安沒有捷徑,請從根本做起!

最近一連串資安事件,加上這兩天看到圈子內很熱門的討論串 (大意: 服務開發商,到底該不該儲存能還原的使用者密碼),於是就想來寫點短篇一點的,說明一下我對軟體開發相關資安 issue 的看法。

那個討論串,我就不提供連結了。我的目的不是要把那位仁兄抓出來鞭,我只是要來聊聊這事件背後的看法而已。如果有人在底下留言附上 link, 我看到會刪掉的,先在此告知

其實我要談的,不是密碼該怎麼加密儲存 (encrypt),或是只儲存加鹽後的雜湊 (salt + hash) 這些技術問題,我想探討的是技術決策或架構設計者的原則跟態度問題。在軟體開發團隊內 (尤其是技術決策者),應該都要有正確的認知,不只是密碼儲存,而是泛指所有資安相關議題。如果你只把資安問題當作一種 “需求規格” 來看待,而非品質要求或是法規要求的話,一旦出事時真的會讓公司陷入困境的。

功能需求 vs 品質要求

其實不只資安問題,很多非功能需求其實都屬於品質要求這維度。例如效能,可靠度,擴充性等等。我用白話一點的例子來說明好了:

功能需求:

按照功能驗收,每一條測試案例都能順利通過,就算符合要求。 通常是正向表列,有列的測試案例就必須通過,你必須解決所有已知問題 (當然列表會隨時間一直追加 XDD)。

品質要求:

除了功能需求之外,必須通過各種壓力、攻擊、滲透等等測試,確保系統在這些極端條件下都還能順利運作,就算符合要求。 通常是負向表列,不能有某些特定的行為出現。例如: 不能顯示未經授權的資料,不能出現 unhandled exception 等等。你必須解決各種未知原因導致出現這些狀況的問題 (當然,這列表也會一直增加無誤 XDD)。

品質需求比較困難的地方,一來較難測試驗證,二來就算發現了也不一定容易修改。到最後你都會發現,很多 “早知道” 當時怎麼設計就可以過關了…。品質問題,就是種典型沒有捷徑的問題,你只能老老實實地從根本做起才能預防。

文章一開始就破題的 “儲存密碼” 問題,這其實就是個品質要求,而不是功能要求。這要求的目的,是希望你的服務即使被外人破解或是竊取資料庫,也不會有密碼外洩的風險。要做到這件事其實不難,只要你的設計不會儲存密碼,小偷就沒有辦法把你的客戶密碼都拿走。

能做到這些 “保證”,不是來自功能需求的規格上有這條測試案例,而是你開發過程在設計 “設定密碼” 時就做好正確的決策,設定密碼時只儲存新密碼加鹽後的 Hash 替代。驗證密碼時將 user 輸入的密碼也照做一次,透過比對計算後的 Hash 來判定密碼是否一致。這過程雖然麻煩了一點,但是整個設計完全不需要儲存原始密碼,也不可能還原 (Hash 是單向的運算),自然也可以完全保證密碼不會因為你的資料庫而外洩。

資安,是信任問題,你必須做 “對的事” 才能換來信任

如同前面的例子,資安類型的問題,都是要求你在某些關鍵資訊的處理一定要到位 (例如密碼、信用卡卡號、交易、個資等等),你才有依據可以保證能做到這要求,讓其他人完全相信你不會有意或無意,破壞客戶的安全。

例如: 只要系統不儲存密碼,就可以免除各種資料庫外洩造成的密碼外洩。

例如: 只要客戶有妥善保管好自己的金鑰,並且用它來進行交易,那麼就可以杜絕別人假冒你的名義進行交易。

例如: 只要你有妥善保管好自己的金鑰,那麼發布出去的資訊只要附上數位簽章,就可以杜絕別人假冒你的名義發布資訊。

例如: 只要雙方都妥善保管好自己的金鑰,雙方就可以在不安全的環境下 (如 internet),安全的交換資料。

為什麼透過加密就能 “保證”? 公開的加解密演算法,難道跟我自己寫的不一樣嗎? 主要有兩個原因:

  1. 公認可靠的加解密演算法 (例如: RSA) 之所以會安全,除了演算法本身可靠之外,主要靠的是金鑰本身。演算法是公開的,程式碼也可以公開,一切只要金鑰保護好就沒事。自己開發的演算法沒經過驗證,也缺乏數學理論的基礎,很容易有破綻會被破解。
  2. 公開的演算法很容易提高強度,只要你挑選了足夠的金鑰長度,並且確認你有好好的保管金鑰,數學就替你保證了在當今的計算能力下,有限的時間內無法破解。

這兩件事,其實就是信任的來源。你可千萬別為了最佳化節省時間,就在這些安全的程序上省略,否則會得不償失的。如果你想不通為何用我自己設計的加密方式,別人根本猜不到啊,怎麼會不安全?

我舉幾個案例你就知道了:

  1. 統計學:
    假設你用了很瞎的字元對照表,把資料重新 mapping 成新的樣子,期待不知道 mapping 的人就解不回來…。其實這不大安全的,有人就這樣破解過,先拿其他資料當作樣本來分析,例如分析一本小說,所有出現的字母, “S” 出現的比率最高,那麼你的土炮加密法出現比率最高的那個字,可能就是 “S” .. 依此類推,可能就被破解了而不自知。(有看過電影: 模仿遊戲嗎? 很有意思的電影,值得看一看)

  2. 程式碼保護:
    在現代的軟體工程裡面,存放 source code 都不夠安全的。程式碼都強調可讀性高,好維護等等特性,目的就是你寫的 code 就是要讓別人好維護啊! 那代表你的 code 就是有別人會看的到,要靠別人看不到你的 code 來保證安全是不可行的。程式碼通常團隊的人都要能看的到 (不然怎麼維護?)。土炮加密通常都是仗著別人不曉得你怎麼加密來保護的;相對於公開安全的演算法,這些演算法都把加解密的過程公開,程式碼也公開。安全性則是來自你必須保護好的金鑰。演算法背後的數學已經證明了你必須持有金鑰才能取得正確資訊,而非程式碼。

剩下的,還是那句話: 該加密就要加密,挑選合適的演算法 + 金鑰長度,你的系統就能讓別人有基本的信任。

由於資安 / 品質問題,往往都被認為是吃力不討好的那一方,做的好別人看不到 (短時間),卻多花了很多資源在上面。因此在時程緊迫的時候往往最容易被犧牲掉。最容易看到這種狀況,通常都是一次性的專案外包…。而願意好好投資在品質身上的,往往都是自己開發自己使用的產品或服務居多,或是標準化大量使用的軟體,有投資在品質的價值的系統。所以,當我看了最前面提到的那討論串,自己的服務不好好顧,還放任團隊設計出會儲存密碼的系統,真的是把自己的信譽當玩笑啊… 要是讓我發現了一次,我絕對不會再去用那套系統 (即使事後官方宣稱已經改善,因為我覺得那是團隊文化問題)。

其他資安案例: 登入 session (認證)

資安領域的常見問題還不少啊,隨便舉都是一堆。以我最近碰到的案子為例,登入 session 的處理也是 (尤其是分散式跨服務的情況)。所謂的 session, 就是當你在登入時通過驗證 (ex: 帳號密碼 + 二階段驗證) 後,直到你登出之前,系統都會認得你,不需要不斷地進行驗證,這就是指 session。

不好處理的地方在於分散式。你經過 A service 驗證,如何讓 B service 承認你的 session ? 當然 create 一把 token 帶過去,B 擔心的話就打 API 問問 A 就可以了。不過往返的通訊成本很高啊,因此業界發展出類似 JWT 這樣標準化的機制,善用加密或是簽章的演算法,你可以省去大部分的通訊,只剩下計算,就能保有一定的安全性。

不過,即使用了 JWT, 你也是有些成本要花費啊! 不論是用現成的框架,或是自己寫,你每個 request 都該做好基本的 token validation (驗證簽章,確認 token 是否被破壞過) 啊! token 驗證無誤,你才能相信裡面夾帶的資訊,包含 expire time, user id 等等資訊,做到不用跟對方通訊就能信任 token 的內容。

Token 的運用,我常常舉火車站驗票的機制當例子。在票證還沒電子化之前,列車長驗票時可沒辦法每張票都打電話跟售票員確認真偽,這時靠的就是小小一張車票本身的各種防偽設計。例如特殊的紙質,編碼規則,甚至是蓋章等等,總之能夠讓列車長當下就能判定即可。確認票是真的,剩下就只要再核對上面的車次跟座位是否符合就可以了。

這邊提到的 session 也是一樣,基礎是省不了的啊! 就像每次收款點鈔時都檢查一下鈔票的防偽設計,確認有沒有拿到假鈔一樣,藉著每個 request 都檢查一次 token 的正確性就能確保每個 request 都是安全的。你也許會覺得這樣要花掉很多加解密的運算,很吃 CPU …,但是我只能說這是必要的;省掉驗證的 request, 就是保護的死角 (除非那個 request 並不會存取重要的資訊)。花費的 CPU 計算能力,可以省去 server 之間的溝通 (通常網路呼叫的效率會比單純的運算慢上數千倍),在安全的前提下是很划算的。

其他案例: 功能的權限管控 (授權)

另一個也是很典型的案例: 權限管理,也是一個常見的大地雷…。所謂大地雷,就是開發人員很容易就自己刻一套安全機制,結果就在不經意的地方,把不該輸出的資料給傳出去了。

舉個常見的例子: 如果按照規則,我的帳號只能看到我部門的員工資訊,結果 100 個功能有一個漏掉忘了檢查,在這個功能就把其他 99 個功能都擋下來的資訊給洩漏出去了。這一個漏洞就會變成老鼠屎,讓整個系統的安全性不合格。

很多網站的安全性,只做在 UI 或是選單,讓使用者點不到就了事了,就是屬於這種狀況。還有一種常見的例子就是,權限檢查只做在第一頁,網站的超連結,很容易點來點去,你就忘了你是從哪邊進來的。有的情況則是兩三個不同的功能入口,會點進同一頁,結果一個入口給你授權,另一個入口不給,那你到這頁共用的頁面,到底能不能看到機密資訊?

有共用資源的都很容易踩到這個坑,把共用頁面換成共用 API,結果也是一樣。這種狀況該怎麼避免? 還是一樣,沒有捷徑,你只能老老實實的做好安全機制。

第一關是老老實實的設計好你的 “安全機制”。你如果不去定義全域的安全規則,把這責任丟給每個功能自己設計,你就很容易踩到矛盾狀況。比如 A 功能讓你看員工資料,B 功能卻禁止,這是系統設計允許使用者設定出不合理的權限造成的。這時你要說他是個 bug 嗎? 有點難,因為你已經騎虎難下了,有問題的是規格,不是實作啊! 跟前面提到演算法一樣,除非你是資安專家,否則你也別自己發明一些奇怪的方法來管理權限,盡量依賴成熟的套件,或是成熟的管理模式來進行。例如 RBAC (Role Based Access Control), 或是一些衍生的機制,如 PBAC (Policy Based Access Control), 或是 ABAC (Attribute Based Access Control)。如果你用 .NET 開發,那善用 .NET 內建的 Authorization 機制也是不錯的選擇。

至於功能上的設計,要老老實實的做,其實也很簡單。別依賴每個功能自己決定授權的機制,你應該整個服務或是模組 (比如部門出缺勤管理),定義一個通用的規範 (例如: 部門主管只能看到自己部門的員工請假狀況),規範夠通用才不會容易犯錯,夠通用你才能夠隨時隨地用一樣的規則來檢驗,太多的特例,通常不用等到 code 出現 bug, 往往規則的設計本身就會存在漏洞了。

其他實作方面,就是工程問題了。控制好資料進出的關卡 (例如 API),越小的接觸面積,越好防護,盡可能的收斂暴露資料的管道。接著,統一識別身分的方式,例如用前面段落提到的 session token 就是個好方法。善用 token 可以確認 “你是誰”,結合上面提到的授權機制 (例如: RBAC), 你就能進一步確認 “你能做什麼”。安全機制的運用站穩腳步後,剩下的就是在每個頁面或是 API 上正確的實作就夠了。要達成安全的要求其實並不難,難的是你有沒有按照規矩施工。

結論: 沒有捷徑,老老實實的實作才是上策

越是安全類的,或是品質、架構類的問題,結論都一樣,沒有捷徑的! 之前看過一句話講得很好:

品質是內建的,不是測試出來的

意思是,你的服務品質好壞,在設計開發階段就決定了。測試只會幫助你找出缺點,而你也只修復缺點的話,那你會得到一個品質不好但是用起來堪用沒問題的服務,不會得到一個高品質的產品的,除非你重新設計..

這是個很貼切的形容啊,也說明了品質相關的問題,你只能從設計之初,就從基礎打好才有可能獲得。這篇文章零零總總,舉了幾個跟資安相關的案例,沒有扯到太深的技術,也沒有貼到 source code (真是難得).. 單純看到 facebook 上面的討論串有感而發,寫了這篇。系統的設計很多地方真的都是省不得的啊! 你也許以為你賺到了,其實那只是暫時的,往後會付出代價的。盡可能別標新立異,盡量套用設計良好的框架來處理安全問題吧! 如果真的有必要自己打造,那請至少用公認標準的 patterns 來設計。

好好的充實自己的經驗能力,老老實實一步一步的實作才是上策啊!






Facebook Pages

Edit Post (Pull Request)

Post Directory