「絕對不要相信來自於使用者的資料」
寫 PHP 時常常會聽到一句「絕對不要相信來自於使用者的資料」,這麼多年過去了,SQL Injection、XSS、LFI 等問題在 PHP 上仍然層出不窮。
有人把原因歸咎於 PHP 的語言設計:過於複雜化的內建函式庫。就算是多年 PHP 經驗的老手,也可能會落入一些不是最佳實踐的陷阱。
SQL Injection 是個很傳統但又不曾過時的議題。想當年,大學必修資料庫課就是靠 SQL Injection 學 SQL
SQL Injection 來自於程式與資料庫溝通時的美麗(?)誤會:
// 假設 DB 這個 class 會動處理資料庫連線等工作。
DB::execute(
sprintf("SELECT * FROM users WHERE name = '%s'", $_GET['name'])
);
當使用 /?name=Jack
時,就可以得到 SELECT * FROM users WHERE name = 'Jack'
的結果,一切看起來相當美好
當使用 /?id=Jack' OR 1=1; --
時,就會得到 SELECT * FORM users WHERE name = 'Jack' OR 1=1; --'
的結果,這時因為 1=1
是 true,所以會隨機取得一名存在於 users 的資料。
PHP 在處理 SQL Injection 共有兩種流派:escape 派與 prepared statement 派。
對於 SQL 參數的 escape 有很多函式可以使用,不止初學者容易混淆,連一些老手也容易落入陷阱。
addslashes()
mysql_escape_string()
mysql_real_escape_string()
mysqli_escape_string()
mysqli_real_escape_string()
註:
mysql_*
相關函式在 PHP 7 已被廢棄,請勿繼續使用
縱覽上面的五個函式,就有四個函式長得相當類似,然後又有 real escape 跟 escape,這真的很容易讓人困惑。
先說結論:只有 _real_
才是真正防止 SQL Injection 的方式,但是 mysqli_escape_string()
跟 mysqli_real_escape_string()
是同一個函式。
Prepared Statement 是比較現代的 Database 所支援的一項 SQL 寫法。
通常會有兩個步驟
這樣的做法有個好處:如果我希望多次執行同一個類似的 SQL Statement,但每次綁定的參數不同,只需要重複進行步驟 2. 即可。
PHP 的實作如下
$dbh = new PDO($dsn, $user, $password);
$sth = $dbh->prepare('SELECT * FROM users WHERE name = ?');
$result = $sth->execute([$_GET['name']]);
基本上,escape 及 prepared statement 都是可以防範 SQL Injection 的手段。然而實際上通常建議以 Prepared Statement 為優先。
知名的論壇管理系統 Discuz! 曾經有過一個 SQL Injection 的漏洞,其成因是在正確 escape 參數之後,又進行字串截斷(因儲存時存在上限),截斷後的資料成為 SQL Injection 的手段。
事實上,預設的 PHP PDO 的 Prepared Statement 並不是走真正的的 Prepared Statement,而是模擬出來的。
詳情可以參閱我之前寫的文章:PHP 騙你 PDO Prepare 並沒有準備好