讀軟體開發安全之道:概念、設計與實施12不受信任的輸入

躺柒發表於2024-08-29

1. 不受信任的輸入

1.1. 不受信任的輸入可能是編寫安全程式碼的開發人員最關心的問題

  • 1.1.1. 最好將其理解為輸入系統中的所有不受信任的輸入

  • 1.1.2. 來自受信任的程式碼的輸入可以提供格式正確的資料

1.2. 不受信任的輸入是指那些不受你控制,並且可能被篡改的資料,包括所有進入系統但你不完全信任的資料

  • 1.2.1. 是你不應該信任的輸入,而不是你錯信了的輸入

1.3. 任何來自外部並進入系統的資料都最好被認為是不受信任的

  • 1.3.1. 不受信任的輸入令人擔憂,因為它們代表了一種攻擊向量,一種能夠進入系統並製造麻煩的途徑

1.4. 全球最大的不可信輸入來源無疑是網際網路

  • 1.4.1. 由於軟體很難完全斷開與網際網路的連線,網際網路幾乎對所有系統都構成嚴重的威脅

2. 輸入驗證

2.1. 輸入驗證(或輸入消毒)是一種防禦性程式碼,它會對輸入的內容施加限制,強制其遵守相應的規則

  • 2.1.1. 輸入驗證是一種很好的防禦措施,因為它會將不受信任的輸入縮減到應用程式可以安全處理的取值範圍內

2.2. 不受信任的輸入通常會穿越系統,並向下延伸到多個受信任的元件中

  • 2.2.1. 僅僅憑藉你的程式碼會從受信任的程式碼中直接呼叫,並不能保證這些輸入是可信的

2.3. 輸入驗證的基本工作是確保不受信任的輸入能夠符合設計規範,以便下游程式碼能處理格式正確的資料

2.4. 我們編寫的幾乎所有程式碼都只能在一個特定的限制內正常工作,它不能被用於極端情況

2.5. 緩解這種危險的一種簡單方法就是對輸入施加人為的限制,排除所有有問題的輸入

  • 2.5.1. 限制當然不應該拒絕那些應該獲得正確處理的輸入

  • 2.5.2. 應該儘快對不受信任的輸入進行驗證,以便能夠最大程度地降低不受約束的輸入流向下游程式碼的風險

  • 2.5.3. 將輸入驗證視為應對不受信任的輸入(特別是攻擊面上的輸入)的防禦機制,但這並不意味著忽視其他的所有輸入

2.6. 關鍵是一致性,因此一個好的模式是在負責處理傳入資料的第一層程式碼中執行輸入驗證,然後將有效的輸入交給更深層的業務邏輯,這些業務邏輯可以自信地認為所有輸入都是有效的

2.7. 寧可在冗餘的輸入驗證上犯錯,也不要面臨產生微小漏洞的風險

  • 2.7.1. 如果你不確定傳入的資料是否經過了可靠驗證,那麼你需要自己執行輸入驗證來確保安全

3. 確定有效性

3.1. 輸入驗證一開始要確定什麼是有效的

  • 3.1.1. 相當於預測未來所有有效的輸入值,並找出合適的理由來禁止其餘的輸入值

3.2. 一旦指定了有效值的範圍,就很容易確定適合程式碼的資料型別

  • 3.2.1. 通常有效的做法是對輸入建立一個明確的限制,然後在實現中留出足夠的餘量,來確保正確地處理所有有效輸入

  • 3.2.2. 餘量是指當你要將一個文字字串複製到4096位元組的緩衝區中時,要將最大的有效長度設定為4000位元組,這樣你就有了一些餘量

  • 3.2.3. 在C語言中,額外的空終止符導致緩衝區溢位1個字元是一個很容易犯的典型錯誤

4. 驗證標準

4.1. 大多數的輸入驗證檢查都包含幾個標準,其中包括確保輸入不會超過最大限制、資料以正確的格式傳入,並且資料值在一個可接受的範圍內

4.2. 檢查值的大小是一種快速測試,主要是為了避免你的程式碼遭受DoS威脅,DoS威脅會導致你的應用程式在接受數兆位元組的不受信任的輸入後,變得執行緩慢甚至崩潰

4.3. 步驟

  • 4.3.1. 首先限制大小,這樣你就不會浪費時間來嘗試處理過大的輸入

  • 4.3.2. 然後在解析之前確保輸入的格式是正確的

  • 4.3.3. 最後檢查結果值是否在可接受的範圍內

4.4. 確定值的有效範圍可能是最主觀的選擇,但重要的是要有具體的限制

  • 4.4.1. 範圍的定義取決於資料型別

  • 4.4.2. 以字元而不是位元組為單位指定字串的最大長度,這樣普通人才可以理解這個約束條件的含義

4.5. 根據某個目的來考慮輸入的有效性會很有幫助

4.6. 選擇一個對使用者更友好的限制會更有意義

4.7. 輸入驗證的主要目的是確保無效輸入不會透過驗證

  • 4.7.1. 最簡單的做法是拒絕無效輸入

  • 4.7.2. 更寬容的選擇是檢測無效輸入並將其修改為有效的形式

5. 拒絕無效輸入

5.1. 拒絕不符合特定規則的輸入,是最簡單並且可以說是最安全的做法

5.2. 完全接受或拒絕是最乾淨妥當的做法,並且通常最容易做對

5.3. 每當人們直接提供輸入(比如填寫Web表格)時,最好能夠提供足夠的關於錯誤的資訊,使他們能夠更輕鬆地糾正錯誤並重新提交

  • 5.3.1. 暫停下來並要求資料來源提供有效的輸入,這是進行輸入驗證的保守做法,它也為普通使用者提供了學習和適應的機會

5.4. 最佳實踐

  • 5.4.1. 解釋有效輸入的構成,至少讓閱讀的人不必猜測並重試

  • 5.4.2. 一次標記多個錯誤,以便使用者能夠一次性更正並重新提交

  • 5.4.3. 當需要人們直接輸入時,保持規則簡單明瞭

  • 5.4.4. 將複雜的表格分成幾個部分,並且每個部分都有一個單獨的表格,這樣人們可以看到事情的進展

5.5. 最佳方法是編寫文件,精確地描述預期的輸入格式和其他約束

5.6. 在專業執行系統的輸入驗證中,會完全拒絕整批輸入,而不是嘗試處理部分有效的資料子集,這種做法可能最合理,因為驗證不透過就表示有些輸入不符合規範

  • 5.6.1. 這樣做允許糾正錯誤並再次提交完整的資料集,而無須梳理出哪些已處理,哪些未處理

6. 糾正無效輸入

6.1. 完全接受有效輸入並拒絕其他輸入,這種做法既安全又簡單,但絕對不是最好的做法

6.2. 如果我們不希望因為微小的錯誤而阻止人們繼續的話,可以透過輸入驗證程式碼來嘗試更正那些無效的輸入,將它們轉換為有效值,而不是直接拒絕輸入

6.3. 根據使用者的輸入,以官方格式提供猜測出的相似地址,以供使用者選擇

6.4. 對於難度較高的驗證需求來說,最好的辦法是將輸入設計得儘可能簡單

6.5. 適當的輸入驗證需要謹慎的判斷,但它也使軟體系統更可靠、更安全

7. 字串漏洞

7.1. 長度問題

  • 7.1.1. 長度是第一個挑戰,因為字串可能是無限長的

  • 7.1.2. 第一道防線是將不受信任的輸入字串的長度限制在合理的範圍內

  • 7.1.3. 在分配緩衝區時,不要將字元數與位元組長度混淆

7.2. Unicode問題

  • 7.2.1. Unicode是一個豐富的字符集,但這種豐富性的代價是隱藏的複雜性,並且這些複雜性會成為漏洞利用的沃土

  • 7.2.2. 大量字元編碼可以將全世界的文字表示為位元組,但大多數軟體會將Unicode作為一種通用語

  • 7.2.2.1. Unicode標準(版本13.0)的長度剛超過1000頁,指定了超過14萬個字元、規範化演算法、舊字元程式碼標準的相容性,以及雙向語言支援

  • 7.2.2.2. 幾乎涵蓋了世界上所有的書面語言,其編碼超過了100萬個程式碼點

  • 7.2.2.3. UTF-8是最常見的編碼,同時還有UTF-7、UTF-16和UTF-32編碼

  • 7.2.3. 排序規則(collation)取決於編碼和語言,如果不關注它的話,就會產生意想不到的結果

  • 7.2.4. 在不需要支援不同的語言環境時,請考慮明確指定其執行的語言環境,而不是繼承系統配置中的設定

  • 7.2.5. 安全性的底線是使用受信任的庫來處理字串,而不是直接對位元組進行處理

  • 7.2.6. Unicode是對字元而不是字形(以何種視覺形式來呈現字元)進行編碼

  • 7.2.7. 規範化文字的一種常用方法是將字串中的字母轉換為大寫或小寫

8. 注入攻擊漏洞

8.1. 一種常見的軟體技術能夠構造一個字串或資料結構(其中編碼了要執行的操作)​,然後執行該字串或資料結構來完成指定的任務

8.2. 如果攻擊者可以改變操作的預期效果,那麼這種影響可能會穿越信任邊界,並由具有更高許可權的軟體執行

  • 8.2.1. 這就是對注入攻擊的解釋

8.3. 包括但不限於

  • 8.3.1. SQL語句

  • 8.3.1.1. SQL隱碼攻擊

  • 8.3.2. 檔案路徑名稱

  • 8.3.3. 正規表示式(作為一種DoS威脅)​

  • 8.3.4. XML資料(尤其是XXE宣告)​

  • 8.3.5. shell命令

  • 8.3.6. 將字串解釋為程式碼(比如JavaScript的eval函式)​

  • 8.3.7. HTML和HTTP頭部

8.4. 路徑遍歷

  • 8.4.1. 檔案路徑遍歷是一個與注入攻擊密切相關的常見漏洞

  • 8.4.2. 這種攻擊不會破壞成對的引號​,而是會進入父目錄,以獲得對檔案系統其他部分的意外訪問

  • 8.4.3. 預防這類攻擊最好的方法是對允許輸入的字符集進行限制

  • 8.4.3.1. 僅由字母和數字構成的字串就足以修復這個漏洞

  • 8.4.3.2. 它排除了從檔案系統預期部分“逃逸”出去所需要的檔案分隔符和父目錄形式

  • 8.4.3.3. 只提供對於某個目錄或其子目錄中檔案的訪問,但絕對不提供對其他位置檔案的訪問

  • 8.4.3.4. base目錄是一個可靠的路徑,因為它不會涉及任何不受信任的輸入:它的輸入完全來自程式設計師控制下的值

8.5. 正規表示式

  • 8.5.1. 正規表示式(regex)具有高效、靈活和易於使用的特點,它提供了非常廣泛的功能,並且可能是最常用來解析文字字串的通用工具

  • 8.5.2. 在編碼和執行上,正規表示式通常比臨時程式碼更快且更可靠

  • 8.5.3. 正規表示式庫會編譯出狀態表,狀態表是一個直譯器(有限狀態機或類似的自動機制)​,能夠執行字串的匹配

  • 8.5.4. 緩解問題的最佳方法取決於具體的計算,但有幾種通用的方法可以用來應對這些攻擊

  • 8.5.4.1. 要避免讓不受信任的輸入影響到有可能崩潰的計算

  • 8.5.4.2. 在使用正規表示式的情況下,不要讓不受信任的輸入來定義正規表示式,儘可能避免回溯,並且限制使用正規表示式匹配的字串的長度

  • 8.5.4.3. 要考慮最糟糕的計算,然後對其進行測試,以確保不會執行得過慢

8.6. XML的危險

  • 8.6.1. XML是表示結構化資料的最流行的方法之一,因為它功能強大且易於閱讀

  • 8.6.2. 將不受信任的輸入排除在你的程式碼所處理的任何XML之外

  • 8.6.3. 如果你不需要XML外部實體,就可以透過在輸入中排除不受信任的輸入,或者禁止處理這類宣告來防止這類攻擊

9. 緩解注入攻擊

9.1. 輸入驗證始終是很好的第一道防線,但考慮到允許的輸入中會包含的內容,僅此一項緩解措施不一定足夠

9.2. 作為額外的防禦層,要研究會形成的命令或語句的語法,並且要確保應用了所有必要的引用或轉義,以確保不會出錯

9.3. 通常可以在原始碼中輕鬆掃描出使注入攻擊成為風險的危險操作

相關文章