自打Web流行以後,面向用戶終端的應用程序和數據庫開始結合,成了動態網站的開發標準架構。但是,這就帶來了一個可怕的漏洞:SQL注入。
在世界最具危險的漏洞排行榜中,SQL常年佔據榜首,是最具危險的漏洞。
為了應對SQL注入的威脅,業界提供了佔位符和SQL連接類庫的prepared語句。這種方式,將可變數據從實際查詢中分離出來,確保兩者不被混合。幾乎所有現代SQL客戶端都支持這個功能,當然,還是可以通過sql語句中混合可變的數據。
為了展示編譯時程序驗證的做法,蟲蟲在此通過Rust語言實例來說明下如何通過Rust特有的生命週期功能來檢查SQL注入漏洞問題。
開始
我們首先創建一個執行SQL查詢的函數。它需要一個SQL查詢和參數值列表以用於佔位符:
fn sql_query(query: &str, params: &[&SQLParam]) -> SQLRows {
// ...
}
為了引入SQL注入漏洞,我們可以動態構建一個format!宏SQL的查詢,Rust的sprintf版本,如下所示:
fn get_user_by_name(username: &str) {
let query = format!("SELECT * FROM users WHERE username={}", username);
let _rows = sql_query(&query, &[]);
// ...
}
這個函數有問題。攻擊者可以提交利用函數中SQL查詢交互的用戶名,在其中注入額外的代碼。代碼可以正常的編譯,只有通過代碼審計才能發現漏洞。
那麼,如何讓我們在編譯就能檢測到這個問題呢?
生命週期
可能你還不熟悉Rust語言,這個語言有一個生命週期的概念。這些是編譯時必須滿足的特殊條件。用它們來防止內存在被引用時被釋放掉。生命週期通常侷限於所引用的變量的詞法作用域。
我們的sql_query的查詢參數是對實際字符串的引用,所以為了確保引用的內存還在被引用的時候不會被釋放掉,所以有了生命週期。生命週期默認由編譯器自動推斷。但是我們可以自己設置生命週期,比如sql_query函數:
fn sql_query(query: &'a str) {
// ...
}
函數的生命週期必須在可以其使用之前聲明。這就是為什麼"'a"出現兩次的原因。一次在查詢類型中,還有函數名稱之後的的聲明中。
'static生命週期
有一個特殊的"'"靜態生命週期值,它可以超過程序的生命週期。
我們可以利用這個特性。如果我們將查詢的生命週期更改為'static,那麼它將只接受與程序同樣生命週期長的字符串:
fn sql_query(query: &'static str) {
// ...
}
這會導致動態字符串不再是有效的參數,因為它們是動態構建的,因此永遠不滿足'static條件。
如果我們試圖編譯之前錯誤的示例代碼,我們會發現它會報錯:
error[E0597]: `query` does not live long enough
--> src/main.rs:6:28
|
6 | let _rows = sql_query(&query, &[]);
| ^^^^^ borrowed value does not live long enough
7 | }
| - borrowed value only lives until here
|
= note: borrowed value must be valid for the static lifetime...
這樣,我們現在無需運行程序就可以成功預防SQL注入漏洞!
要修復這個問題,我們必須提供一個靜態字符串並使用佔位符?做為用戶名。
let _rows = sql_query("SELECT * FROM users WHERE username=?", &[username]);
因為查詢字符串現在是一個字符串,所以它滿足'static生命週期,允許程序編譯。
結論
Rust是一種具有許多有趣功能的語言,它們可以幫助程序員編寫靜態正確性檢查的代碼。這也給了它學習起來有點燒腦,但是一旦你掌握了這們神奇的語言,你也就不太可能再犯SQL注入這樣的錯誤了。
我們在這裡通過一個實例證明了靜態程序分析的強大和用途,以及它如何幫助碼農們寫出更好更安全的軟件。
閱讀更多 蟲蟲安全 的文章