PHP 程式設計/SQL 注入攻擊
考慮以下 PHP 中的 SQL 查詢
$result=mysql_query('SELECT * FROM users WHERE username="'.$_GET['username'].'"');
該查詢從 users 表中選擇所有使用者名稱等於查詢字串中使用者名稱的那一行。如果你仔細觀察,你會發現該語句容易受到 SQL 注入的攻擊 - $_GET['username'] 中的引號沒有轉義,因此將被連線到語句中,這會導致惡意行為。
考慮如果 $_GET['username'] 是以下內容會發生什麼:" OR 1 OR username = " (一個雙引號,後跟文字 " OR 1 OR username = ",後面是另一個雙引號)。當連線到原始表示式時,你將得到一個這樣的查詢:SELECT * FROM users WHERE username = "" OR 1 OR username = ""。看似冗餘的 OR username = " 部分是為了確保 SQL 語句在沒有錯誤的情況下進行評估。否則,在語句的末尾會留下一個懸掛的雙引號。
這將從 users 表中選擇所有行。
永遠不要相信使用者提供的資料,只有在驗證之後才能處理這些資料;通常,這是透過模式匹配來完成的。在下面的示例中,使用者名稱被限制為字母數字字元加下劃線,長度在 8 到 20 個字元之間 - 根據需要進行修改。
if (preg_match("/^\w{8,20}$/", $_GET['username'], $matches))
$result = mysql_query("SELECT * FROM users WHERE username='$matches[0]'");
else // we don't bother querying the database
echo "username not accepted";
為了提高安全性,你可能希望透過用 exit() 或 die() 替換 echo 來終止指令碼的執行。
此問題在使用複選框、單選按鈕、選擇列表等時仍然適用。任何瀏覽器請求(即使是 POST)都可以透過 telnet、複製網站、javascript 或程式碼(甚至是 PHP)複製,因此始終要注意在客戶端程式碼上設定的任何限制。
PHP 提供了一個函式來處理 MySQL 中的使用者輸入,那就是 mysqli_real_escape_string([mysqli 連線, ]字串 unescaped_string)。該指令碼轉義提供的字串中所有潛在的危險字元,並返回轉義後的字串,使其可以安全地放入 MySQL 查詢中。但是,如果你在將輸入傳遞到 mysqli_real_escape_string() 函式之前沒有對其進行清理,你仍然可能有 SQL 注入向量。例如;mysqli_real_escape_string 不會防禦諸如以下的 SQL 注入向量
$result = "SELECT fields FROM table WHERE id = ".mysqli_real_escape_string($_POST['id']);
如果 $_POST['id'] 包含 23 OR 1=1,則生成的查詢將是
SELECT fields FROM table WHERE id = 23 OR 1=1
這是一個有效的 SQL 注入向量。
(原始函式 mysql_escape_string 沒有考慮當前字元集來跳脫字元串,也沒有接受連線引數。從 PHP 4.3.0 開始,它已被棄用。)
例如,考慮上面的例子之一
$result=mysqli_query($link, 'SELECT * FROM users WHERE username="'.$_GET['username'].'"');
這可以這樣轉義
$result=mysqli_query($link, 'SELECT * FROM users WHERE username="'.mysqli_real_escape_string($_GET['username']).'"');
這樣,如果使用者試圖注入另一個語句,例如 DELETE,它將被無害地解釋為 WHERE 子句引數的一部分,就像預期的那樣
SELECT * FROM `users` WHERE username = '\';DELETE FROM `forum` WHERE title != \''
mysqli_real_escape_string 新增的反斜槓使 MySQL 將它們解釋為實際的單引號字元,而不是作為 SQL 語句的一部分。
請注意,MySQL 不允許堆疊查詢,因此 ;DELETE FROM table 攻擊將無法執行
PEAR 的 DB 包[1] 提供了 prepare/execute 機制來執行引數化語句。
require_once("DB.php");
$db = &DB::connect("mysql://user:pass@host/database1");
$p = $db->prepare("SELECT * FROM users WHERE username = ?");
$db->execute( $p, array($_GET['username']) );
query() 方法,也與 prepare/execute 相同,
$db->query( "SELECT * FROM users WHERE username = ?", array($_GET['username']) );
prepare/execute 將自動呼叫 mysql_real_escape_string(),如上文所述。
在 PHP 版本 5 和 MySQL 版本 4.1 及更高版本中,還可以透過 mysqli 擴充套件[2] 使用預備語句。例子[3]
$db = new mysqli("localhost", "user", "pass", "database");
$stmt = $db -> prepare("SELECT priv FROM testUsers WHERE username=? AND password=?");
$stmt -> bind_param("ss", $user, $pass);
$stmt -> execute();
類似地,你也可以使用 PHP5 中的內建 PDO 類[4]。
- ↑ http://pear.php.net/package/DB
- ↑ Mysqli 擴充套件的官方文件,php.net。
- ↑ PHP 和 MySQLi 中的預備語句,Matt Bango。
- ↑ https://php.net.tw/manual/en/book.pdo.php