最近在工作中遇到一個 SQL 注入,雖然影響不是很大,但也足夠讓我靜下來思考,SQL 注入問題好像很簡單、好像每個人都明白,但在實際的開發(fā)中還是會出現(xiàn)這樣低級的錯誤,出現(xiàn)這樣的問題個人覺得和 PHP 開發(fā)隨意化有關,第二個原因可能是沒有明白 SQL 注入的原理。
什么是 SQL 注入
在應用程序中,為了和用戶交互,允許用戶輸入數(shù)據(jù),假如開發(fā)者不慎重出現(xiàn)漏洞,攻擊者輸入一些特殊的字符從而讓應用程序執(zhí)行了危險的 SQL 操作。危險可大可小,小的泄漏了機密數(shù)據(jù)(比如用戶信息),大的危害可能會刪除整個數(shù)據(jù)庫。
SQL 注入根本的原因就在于被攻擊者構建了危險的 SQL,看 PHP 中的一個例子:
$sql = "select * from tb where name='" . $name . "'";
mysql_query($sql);
這時候假如 $name 的值被惡意輸入 1' OR '1'='1,則最后的 SQL 就是select * from tb where name='1' OR '1'='1',從而查詢出了所有的表數(shù)據(jù),這就是比較簡單的 SQL 注入。
構建危險 SQL 的主要原因在于 SQL 語句中的“雙引號”、“單引號”、“關鍵字”。利用這些字符攻擊者可以構建出很多其他語義的 SQL。
如何避免 SQL 注入
上面說到 SQL 注入的原因在于“雙引號”、“單引號”、“關鍵字”,所以預防的方法就在于“破壞”這些特殊字符。主要有兩種方法:
(1)過濾數(shù)據(jù)
在開發(fā)程序的時候,根據(jù)應用的限制,應該明確規(guī)定用戶輸入的數(shù)據(jù)是什么類型的,比如是字符串,還是整型,或者是富文本數(shù)據(jù),應用程序就必須嚴格的限制,比如某些輸入假如過濾了“單引號”等特殊符號,自然就構建不起危險的 SQL 了。
為什么在 PHP 語言中會出現(xiàn)很多的低級操作呢?有部分原因就在于 PHP 太靈活了,比如在數(shù)據(jù)層直接使用$_GET變量的值構建 SQL,從而導致問題。當然這不完全是語言的問題,主要在于語言結構太靈活,開發(fā)者肆無忌憚導致的。
(2)轉義數(shù)據(jù)
由于“雙引號”、“單引號”等特殊字符在 SQL 語句中有特殊的能力(指令),所以用戶輸入的這些特殊符號在構建 SQL 的時候就也有了特殊的含義,假如我們通過一種方式讓用戶輸入的特殊符號沒有其特殊能力(只有字符本身的含義),那么就不會出現(xiàn) SQL 注入了,這種方式就是轉義,在 MySql 中內(nèi)置了這樣的函數(shù) mysql_real_escape_string()。
SQL 中 select/update/delete/insert 語句都需要轉義,對于 insert 語句來說,轉義不代表插入的數(shù)據(jù)在表中多了個反斜杠字符,這個概念一定要清楚,在數(shù)據(jù)庫存儲里面是不會放入反斜杠轉義符號的。
接下來重點看看如何在 PHP 中進行轉義數(shù)據(jù)。
mysql(mysqli) 擴展庫
PHP 語言是從 C 語言發(fā)展而來的,所以早期其實很多函數(shù)和 C 語言函數(shù)同名,這也說明大部分 PHP 函數(shù)的實現(xiàn)就是調(diào)用 C 語言函數(shù)庫。MySql 擴展就是典型的一個函數(shù)庫(調(diào)用本地的 C 命令函數(shù)庫)。
PHP 中的 mysql_real_escape_string() 函數(shù)就是執(zhí)行轉義的,只要在輸入的數(shù)據(jù)變量上調(diào)用這個函數(shù),就能解決大部分注入問題。
這個函數(shù)很簡單,這里說說一個很古老的函數(shù) addslashes(),在我早期使用 PHP 過程中,經(jīng)常遇到這個函數(shù),它也能對字符串進行轉義,所以很多人用它代替 mysql_real_escape_string() 函數(shù),其實這是不對的一種方法,因為兩者轉義的具體字符是不一樣的,同時 addslashes() 不并是專門用戶轉義數(shù)據(jù)庫中的字符。
拋開數(shù)據(jù)庫操作來說,在如今的 PHP 中也不建議使用 addslashes() 函數(shù),在 PHP 早期版本中,$_GET
或$_POST
數(shù)據(jù)會自動進行 addslashes() 調(diào)用(猜測是為了保證數(shù)據(jù)的安全),可也破壞了一個規(guī)則——不應該修改用戶的原始數(shù)據(jù),所以在新版本 PHP 中,這個函數(shù)對應的 magic_quotes_gpc 指令是關閉的,但是從我的角度來看,PHP 開發(fā)者應該盡量去忘記這個函數(shù)的存在。
PDO
如何理解 PDO 呢?可以看看 mysqli 包的一些弱點:
PHP 開發(fā)者可能會操作 MySql 或者其他類型的數(shù)據(jù)庫,但是 mysqli 庫只能操作 MySql,對于 PHP 開發(fā)者來說沒有統(tǒng)一的接口去操作 SQL 語言(注意不是操作數(shù)據(jù)庫)。
mysqli 上的一些 SQL 操作功能缺乏,比如沒有事務處理等等。
而 PDO 就能解決上述兩個問題,而具體到轉義操作上,PDO 通過Prepared Queries讓你來構建更安全的 SQL 語句,對于 mysql_real_escape_string() 來說開發(fā)者還是需要自己構建 SQL 語句,而 PDO 可以提供更優(yōu)雅的方式操作 SQL。
$stmt = $db->prepare("SELECT * FROM table WHERE id=:id AND name=:name");
$stmt->execute(array(':name' => $name, ':id' => $id));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt = $db->prepare("UPDATE table SET name=? WHERE id=?");
$stmt->execute(array($name, $id));
$affected_rows = $stmt->rowCount();