為什麼開發者不編寫安全的程式碼?我們在這並不是要再一次討論「整潔程式碼」。我們要從純粹的實用觀點出發,討論軟體的安全性和保密性。是的,因為不安全的軟體不僅無用,而且還可怕。我們來看看什麼是不安全的軟體。
- 1996年6月4日,歐洲航天局的 Ariane 5 Flight 501 在起飛後 40 秒被引爆。因為導航軟體裡的一個 bug,這個價值 10 億美金的運載火箭不得不自毀。
- 1991年2月25日,MIM-104 Patriot(愛國者)裡的一個軟體錯誤使它的系統每一百小時有三分之一秒的時鐘偏移,導致定位攔截入侵導彈失敗。結果伊拉克的飛毛腿導彈擊中宰赫蘭(沙烏地阿拉伯東北部城市)的一個美軍軍營,28 人死亡,100 多人受傷。
- 其他案例,請參見《Bug 引發的 18 次重大事故》。
這應該能夠說明編寫安全軟體的重要性了,尤其在特定的環境中。當然也包括其他用例中,我們也應該意識到我們的軟體 bug 會導致什麼後果。
防禦性程式設計初窺
為什麼我認為在特定種類的工程中,防禦性程式設計是解決這些問題好辦法?
抵禦那些不可能的事,因為看似不可能的事也會發生。
防禦性程式設計中有很多防禦方式,這也取決於你的軟體專案所需的「安全」級別和資源級別。
防禦性程式設計是防禦式設計的一種形式,用來確保軟體在未知的環境中能繼續執行。防禦性程式設計的實踐往往用於需要高可用性、安全性、保密性的地方。—— 維基百科
我個人相信這種方法適合很多人蔘與的大型、長期的專案。例如,一個需要大量維護的開源專案。
我們來探索一下我提出的關鍵點,來完成一個防禦性程式設計的實現。
永遠不要相信使用者輸入
設想你總是獲取到你不想要的東西。因為像我們說過的,我們預期的是異常情況的出現,(所以)要時刻防備使用者輸入以及通常會傳入你係統的東西,這是你成為一個防禦性程式設計師的方法。試著做到儘可能的嚴格,確保輸入的值就是你所期望的值。
進攻是最好的防守
設定白名單而不是黑名單。舉個例子,當你驗證影像副檔名時,不要檢查非法的型別,而是檢查合法的型別並排除其他型別。在 PHP 有無數的開源校驗庫可以讓你的工作變簡單。
進攻是最好的防守。共勉
資料庫抽象化
在 OWASP Top 10 Security Vulnerabilities 排首位的是注入攻擊。這意味著有些人(很多人)還沒有使用安全的工具來查詢資料庫。請使用資料庫抽象包或庫。在 PHP 裡你可以使用 PDO 來確保基本的注入攻擊防範。
不要重複發明輪子
你不用框架(或微框架)嗎?好吧恭喜你,你喜歡毫無理由地做額外的工作。這並不僅跟框架有關,也意味著你可以方便地使用已經存在的、經過測試的、受萬千開發者信任的、穩定的新特性,而不是你只為了自己從中受益而製作的東西。你自己建立方法的唯一原因是你需要的東西不存在,或存在但不符合你的需求(效能差、缺失特性等等)。
這就是所謂的智慧程式碼重用。擁抱它吧。
不要相信開發者
防禦性程式設計與防禦性駕駛相關聯。在防禦性駕駛中,我們假設周圍的每個人都可能犯錯。所以我們要留意別人的行為。相同概念也適用於防禦性程式設計,我們作為開發者不要相信其他開發者的程式碼。我們同樣也不要相信我們的程式碼。
在很多人蔘與的大型專案中,我們有許多方式編寫並組織程式碼。這也導致混亂甚至更多的 bug。這也是我們需要加強規範程式碼風格和程式碼檢查的原因,讓生活更輕鬆。
編寫符合 SOLID 原則的程式碼
這是(防禦性)程式設計最困難的部分——編寫不糟糕的程式碼。這也是很多人知道並一直在討論的,但沒有人真正關心或將注意力和精力放在實現符合 SOLID 原則的程式碼上。
讓我們看一些糟糕的例子
避免:未初始化的屬性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php class BankAccount { protected $currency = null; public function setCurrency($currency) { ... } public function payTo(Account $to, $amount) { // sorry for this silly example $this->transaction->process($to, $amount, $this->currency); } } // I forgot to call $bankAccount->setCurrency('GBP'); $bankAccount->payTo($joe, 100); |
在這個例子中,我們需要牢記簽發付款前要先呼叫 setCurrency。這是很糟糕的事情,一個像這樣的改變狀態的操作(簽發付款)不應該分兩步來完成,且使用兩個公開的方法。我們還可以用很多方法付款,但我們必須只有一個公開的方法來改變狀態(物件不應該存在不一致的狀態)。
在這個例子中,我們把它改進,將未初始化的屬性封裝進 Money 物件。
1 2 3 4 5 6 7 8 |
<?php class BankAccount { public function payTo(Account $to, Money $money) { ... } } $bankAccount->payTo($joe, new Money(100, new Currency('GBP'))); |
使它萬無一失。不要使用未初始化的物件屬性。
避免:在類的作用域外洩露狀態
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?php class Message { protected $content; public function setContent($content) { $this->content = $content; } } class Mailer { protected $message; public function __construct(Message $message) { $this->message = $message; } public function sendMessage( { var_dump($this->message); } } $message = new Message(); $message->setContent("bob message"); $joeMailer = new Mailer($message); $message->setContent("joe message"); $bobMailer = new Mailer($message); $joeMailer->sendMessage(); $bobMailer->sendMessage(); |
在這個例子中,Message 是通過引用傳遞的,兩個例項的輸出都是 “joe message”。一個解決方案是複製 Mailer 建構函式中的 message 物件。但是我們應該做的是試著使用(不可變的)值物件,而不是簡單可變的 Message 物件。儘可能使用不可變的物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<?php class Message { protected $content; public function __construct($content) { $this->content = $content; } } class Mailer { protected $message; public function __construct(Message $message) { $this->message = $message; } public function sendMessage() { var_dump($this->message); } } $joeMailer = new Mailer(new Message("bob message")); $bobMailer = new Mailer(new Message("joe message")); $joeMailer->sendMessage(); $bobMailer->sendMessage(); |
編寫測試
這點我們很還需要再說嗎?編寫單元測試可以幫助你秉承一般的原則,比如高內聚、單一職責、低耦合和正確的物件組合。它不僅幫助你測試小的單元用例,也能測試你組織物件的方式。確實,當測試你的小功能時,你會清晰的看到你需要測試多少情況和需要模擬多少物件,來達到 100% 的覆蓋率。
結論
希望你喜歡這篇文章。記住這些僅僅是建議而已,由你決定何時、何處以及是否應用。
感謝閱讀!
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式