SQL Injection(SQL 注入)是一種常見且危險的網路應用程式安全漏洞。它利用了應用程式在處理使用者輸入時的疏忽,讓惡意使用者能夠在應用程式的 SQL 查詢中插入惡意的 SQL 程式碼,從而操縱資料庫,達到未經授權的存取、修改、刪除甚至控制整個資料庫的目的。
什麼是 SQL Injection?
SQL Injection 的核心原理是:應用程式將不可信的使用者輸入直接或間接地拼接到 SQL 查詢字串中,而沒有進行適當的驗證或轉義。
這會導致資料庫將使用者輸入的惡意字串視為 SQL 語句的一部分來執行,而不是單純的資料。
舉例說明:
假設你的網站有一個登入功能,後端程式碼可能是這樣構建 SQL 查詢的:
// PHP 範例
$username = $_POST['username']; // 從使用者輸入獲取用戶名
$password = $_POST['password']; // 從使用者輸入獲取密碼
$sql = "SELECT * FROM users WHERE username = '" . $username . "' AND password = '" . $password . "'";
// 執行這個 SQL 查詢...
如果使用者在 username
欄位輸入 admin
,在 password
欄位輸入 mypass
,則生成的 SQL 查詢會是:
SELECT * FROM users WHERE username = 'admin' AND password = 'mypass';
這看起來沒問題。但如果惡意使用者在 username
欄位輸入 admin' OR '1'='1
,在 password
欄位隨意輸入(例如 whatever
),那麼生成的 SQL 查詢將會是:
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'whatever';
請注意 admin' OR '1'='1
如何改變了查詢的結構:
- 第一個單引號
'
關閉了username
的字串。 OR '1'='1'
是一個永遠為真的條件。- 後面的
--
(在某些資料庫中是#
或/*...*/
) 是 SQL 的註解符號,它會將查詢中剩餘的部分(例如AND password = 'whatever'
)變成註解,從而使其失效。
現在,無論 password
是什麼,OR '1'='1'
這個條件都為真,導致資料庫會返回 users
表中的所有行(或者第一行符合 admin
的使用者),讓攻擊者繞過密碼驗證成功登入。
更具破壞性的攻擊:
攻擊者還可以利用 SQL Injection 執行更危險的操作:
- 竊取資料:
SELECT * FROM users WHERE id = '1' UNION SELECT username, password FROM admins; --
- 刪除資料:
DELETE FROM products WHERE id = '1'; DROP TABLE users; --
- 修改資料:
UPDATE products SET price = 0 WHERE id = '1'; --
- 執行系統命令 (如果資料庫和權限允許):
EXEC xp_cmdshell 'dir c:\'; --
(針對 SQL Server)
如何防範 SQL Injection?
防範 SQL Injection 的最有效方法是永遠不要將使用者輸入直接拼接或構造到 SQL 查詢中。以下是主要的防範策略:
1. 使用參數化查詢 (Parameterized Queries) 或預處理語句 (Prepared Statements) - 最推薦且最有效
這是防範 SQL Injection 的黃金法則。它將 SQL 程式碼和使用者輸入的資料完全分離。你為 SQL 查詢中的每個變數創建一個佔位符,然後將使用者輸入作為參數綁定到這些佔位符上。
原理: 資料庫會先解析帶有佔位符的 SQL 語句,然後再將參數的值安全地傳遞給它。資料庫會將這些參數視為純粹的資料,而不是可執行的程式碼。即使參數中包含 SQL 關鍵字或攻擊字串,它們也只會被當作字串值處理。
PHP 範例 (使用 PDO):
// 連接到資料庫
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'username', 'password');
// 準備 SQL 語句,使用 ? 作為佔位符
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
// 綁定參數。資料庫會將這些值視為字串,即使它們包含惡意字元。
$stmt->bindParam(1, $username); // 這裡的 $username 是使用者輸入
$stmt->bindParam(2, $password); // 這裡的 $password 是使用者輸入
// 執行語句
$stmt->execute();
// 獲取結果
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user) {
echo "登入成功!";
} else {
echo "登入失敗。";
}
2. 使用 ORM (Object-Relational Mapping) 框架
諸如 Laravel 的 Eloquent ORM、Doctrine (PHP)、Hibernate (Java)、SQLAlchemy (Python) 等現代 ORM 框架,底層通常都使用參數化查詢來與資料庫互動,因此它們能天然地防範 SQL Injection。
Laravel Eloquent 範例:
// 假設 User 是 Eloquent Model
$username = $request->input('username');
$password = $request->input('password');
// Eloquent 會自動使用參數化查詢
$user = User::where('username', $username)
->where('password', $password)
->first();
if ($user) {
// 登入成功
} else {
// 登入失敗
}
3. 輸入驗證 (Input Validation) 和清理 (Sanitization)
雖然參數化查詢是主要防禦手段,但良好的輸入驗證和清理仍然是重要的防禦深度。
- 驗證 (Validation):
- 白名單驗證 (Whitelist Validation): 只允許已知的、安全的輸入。例如,如果一個欄位應該是數字,就只允許數字。如果一個欄位應該是電子郵件,就只允許合法的電子郵件格式。這是最推薦的方式。
- 黑名單驗證 (Blacklist Validation): 禁用已知的惡意字元或模式(例如
'
,;
,--
,OR
等)。這種方式不夠可靠,因為攻擊者總能找到繞過黑名單的方法。
- 清理 (Sanitization):
- 移除或轉義輸入中可能造成危害的特殊字元。例如,將單引號
'
轉換為''
(兩個單引號) 或\'
(帶有轉義符的單引號)。 - 重要: 僅僅轉義輸入並不能完全防範 SQL Injection,因為轉義規則可能因資料庫類型或編碼而異,且容易出錯。參數化查詢才是王道。
- 移除或轉義輸入中可能造成危害的特殊字元。例如,將單引號
4. 最小權限原則 (Principle of Least Privilege)
- 限制資料庫使用者的權限: 應用程式連接資料庫時使用的帳戶,只應擁有完成其任務所需的最小權限。例如,如果應用程式只需要讀取資料,就不要給予
INSERT
,UPDATE
,DELETE
或DROP TABLE
等權限。 - 避免使用高權限帳戶: 絕不使用資料庫管理員帳戶 (如
root
,sa
) 來運行應用程式。
5. 關閉錯誤訊息 (Error Messages)
- 在生產環境中,不要將詳細的資料庫錯誤訊息直接顯示給使用者。這些錯誤訊息可能包含有助於攻擊者理解資料庫結構或漏洞的資訊。
- 應將錯誤記錄到日誌檔案中,並向使用者顯示一個通用的、友好的錯誤提示。
6. 定期更新和修補
- 保持你的應用程式框架、資料庫系統、驅動程式和所有相關軟體庫都是最新版本。開發者會不斷發布安全補丁來修復已知漏洞。
7. Web Application Firewall (WAF)
- WAF 可以在網路層面提供額外的保護。它會檢查進出應用程式的 HTTP 請求,並嘗試識別和阻止惡意流量,包括 SQL Injection 攻擊。雖然 WAF 不能替代程式碼層面的防護,但它可以作為一道額外的防線。
總結
防範 SQL Injection 最核心、最有效的措施是使用參數化查詢或預處理語句。結合輸入驗證、最小權限原則和適當的錯誤處理,可以大大提高應用程式的安全性。
沒有留言:
張貼留言