什麼是 SQL 注入

「有人的地方就有江湖,有資料庫存在的地方就可能存在 SQL 注入漏洞。」

在所有漏洞類型中,SQL 注入可是說是危害最大最受大家關注的漏洞。簡單說來,SQL 注入是通過在用戶可控參數中注入SQL語法,破壞原有SQL結構,達到編寫程序時意料之外結果的攻擊行為。還是以 ThinkJS 為例,假設我們寫瞭如下一個介面(實際情況肯定不會這麼寫的):

// user.js
module.exports = class extends think.Controller {
async loginAction() {
const { username, password } = this.post();
const user = await this.model().query(
`SELECT * FROM user WHERE name = "${username}" AND password= "${password}"`
);

if (think.isEmpty(user)) {
return this.fail();
}
return this.success(user);
}
}

當用戶提交的 usernameadmin"; -- 的話,最終執行的 SQL 語句就會變成

SELECT * FROM user WHERE name = "admin"; --" AND password= "111"

最終攻擊者就可以成功登錄 admin 賬號了,這就是最簡單的 SQL 注入了。從上面這個簡單示例中,我們發現漏洞成因可以歸結為以下兩個原因疊加造成的:

  1. 程序編寫者在處理應用程序和資料庫交互時,使用字元串拼接的方式構造SQL語句。
  2. 未對用戶可控參數進行足夠的過濾便將參數內容拼接進入到SQL語句中。

SQL注入根據攻擊者獲取數據的方式分為回顯注入報錯注入以及盲注。剛才演示的直接從返回結果中獲取數據則為回顯注入,當然也可以通過 MySQL 執行的報錯結果中嗅探到資料庫的結構和內容,這就是報錯注入。盲注則是根據資料庫執行的延時等操作來判斷是否接近正確值,簡單的說來有點像是拿著聽診器試探保險箱的密碼的感覺。

不同的分類原則會有不同的分類,也有按照注入位置及方式不同進行分類分為POST注入GET注入cookie注入盲注延時注入搜索注入base64注入等。不過大家都支持分類形式不同,原理還是一致的,這裡就不一一細說了。

SQL 注入的危害

如果網站存在 SQL 注入漏洞,相當於將資料庫直接暴露在攻擊者面前,可想而知危害會有多大了。攻擊者利用 SQL 注入漏洞能實現以下攻擊:

  1. 跳過賬戶許可權驗證達到越權
  2. 獲取資料庫關鍵信息從而進行脫庫
  3. 在特別情況下還可以修改資料庫內容或者插入內容到資料庫,如果資料庫許可權分配存在問題,或者資料庫本身存在缺陷,那麼攻擊者可以通過SQL注入漏洞直接獲取webshell或者伺服器系統許可權。

防禦方法

數據校驗

從文章開頭可以看到,其實漏洞的主要原因還是沒有對用戶輸入的數據進行過濾,所以對來自用戶的數據(GET, POST, cookie 等)最好做到以下兩種過濾校驗

  1. 檢查輸入的數據是否具有所期望的數據格式。這種在參數是數字的時候特別有效,如果攻擊者選擇在參數中插入內容的話則會被轉換成 NaN 導致攻擊失敗。在 ThinkJS 中我們提供了強大的 Logic 功能可以方便的對數據進行格式校驗。
  2. 使用資料庫特定的敏感字元轉義函數把用戶提交上來的非數字數據進行轉義。在 ThinkJS 中封裝了 escapeString() 方法可以對敏感字元進行轉義,其原理則和 PHP 的 mysql_escape_string() 方法是一致的。

檢查輸入數據格式在 ThinkJS 中還能防止另外一種非通用 SQL 安全問題。文章開頭的示例代碼我們在實際的應用中一般會這麼寫:

// user.js
module.exports = class extends think.Controller {
async loginAction() {
const { username, password } = this.post();
const user = await this.model(user).where({
name: username,
password
}).find();

if (think.isEmpty(user)) {
return this.fail();
}
return this.success(user);
}
}

當我們構造如 name=admin&password[]=!%3D&password[]= 的請求參數時,最終執行的 Model 語句就會變成

this.model(user).where({name: admin, password: [!=, ]});

由於 HTTP 請求的自動合併數組的特性造成了我們的 SQL 語句並非是我們想要的效果。雖然說框架本身已經針對這種情況進行了處理,當用戶輸入參數被認為是 SQL 運算符時則會將關鍵字增加空格,從而將其變成普通字元串避免這個問題。不過這種方法會有 一定的損傷,畢竟當真的要傳這幾個運算符的情況的時候接收到的數據和請求的不一樣還是有點懵逼的。所以最好還是在 Logic 層對數據進行完善的校驗將問題前置比較好。

除了數據校驗,也可以選擇使用資料庫的存儲過程和預定義指針等特性來抽象數庫訪問,使用戶不能直接訪問數據表和視圖。但這個辦法又有別的影響。

via: SQL注入

許可權限制

嚴格限制Web應用的資料庫的操作許可權,給此用戶提供僅僅能夠滿足其工作的最低許可權,從而最大限度的減少注入攻擊對資料庫的危害。請記住永遠不要使用超級用戶或所有者帳號去連接資料庫!當資料庫被攻擊時將損傷限制在當前表的範圍是比較明智的選擇。通過許可權限制可以防止攻擊者獲取資料庫其它信息,甚至利用資料庫執行 Shell 命令等操作。

日誌處理

當資料庫操作失敗的時候,盡量不要將原始錯誤日誌返回,比如類型錯誤、欄位不匹配等,把代碼裏的 SQL 語句暴露出來,以防止攻擊者利用這些錯誤信息進行 SQL 注入。除此之外,在允許的情況下,使用代碼或資料庫系統保存查詢日誌也是一個好辦法。顯然,日誌並不能防止任何攻擊,但定期審計資料庫執行日誌可以跟蹤是否存在應用程序正常邏輯之外的 SQL 語句執行。日誌本身沒用,要查閱其中包含的信息纔行。畢竟,更多的信息總比沒有要好。

後記

綜上所說之後,大家可能覺得 SQL 數據校驗會比較麻煩,其實在 ThinkJS 中已經將關鍵字處理類的方法已經集成,使用程序提供的 ORM 方法進行 SQL 構造會比自己寫 SQL 語句拼接來的更方便,同時也能提高項目代碼復用,減少潛在的風險。如果對 ThinkJS 默認的 think-model 不喜歡的話,也可以使用其它第三方的 ORM 框架,例如 think-sequelize

參考資料:

  • 《SQL注入》
  • 《SQL注入的原理與分類》
  • 《避免SQL注入》
  • 《Making a javascript string sql friendly》

推薦閱讀:

相關文章