跳轉到內容

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 link, ]string 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[4] 中的內建 PDO 類。

參考資料

[編輯 | 編輯原始碼]

更多資訊

[編輯 | 編輯原始碼]


華夏公益教科書