轉(zhuǎn)載自:PHP防SQL注入不要再用addslashes和mysql_real_escape_string
看了很多PHP網(wǎng)站在防SQL注入上還在使用addslashes和str_replace,百度一下"PHP防注入"也同樣在使用他們只锻,實踐發(fā)現(xiàn)就連mysql_real_escape_string也有黑客可以繞過的辦法欺缘,如果你的系統(tǒng)仍在用上面三個方法但荤,那么我的這篇博文就有了意義贰镣,以提醒所有后來者繞過這個坑。
出于為后人栽樹而不是挖坑的考慮呼寸,給出PHP以及MYSQL的版本信息识樱,以免將來“問題”不再是“問題”了嗤无。
用str_replace以及各種php字符替換函數(shù)來防注入已經(jīng)不用我說了,這種“黑名單”式的防御已經(jīng)被證明是經(jīng)不起時間考驗的怜庸。
下面給出繞過addslasher和mysql_real_escape_string的方法(Trick)当犯。
注意:雖然在MYSQL5.5.37-log下該Trick已經(jīng)被修復了,但仍然沒有確切地解決注入問題割疾,介于很多公司的系統(tǒng)仍在使用Mysql5.0嚎卫,我建議立馬做出改進.
注意:如果你不確定你的系統(tǒng)是否有SQL注入的風險,請將下面的下面的DEMO部署到你的服務器宏榕,如果運行結(jié)果相同拓诸,那么請參考最后的完美的解決方案侵佃。
MYSQL:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word;">mysql> select version();
+---------------------+
| version() |
+---------------------+
| 5.0.45-community-ny |
+---------------------+
1 row in set (0.00 sec)
mysql> create database test default charset GBK;
Query OK, 1 row affected (0.00 sec)
mysql> use test;
Database changed
mysql> CREATE TABLE users (
username VARCHAR(32) CHARACTER SET GBK,
password VARCHAR(32) CHARACTER SET GBK,
PRIMARY KEY (username)
);
Query OK, 0 rows affected (0.02 sec)
mysql> insert into users SET username='ewrfg', password='wer44';
Query OK, 1 row affected (0.01 sec)
mysql> insert into users SET username='ewrfg2', password='wer443';
Query OK, 1 row affected (0.01 sec)
mysql> insert into users SET username='ewrfg4', password='wer4434';
Query OK, 1 row affected (0.01 sec)=</pre>
PHP:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word;"><?php
echo "PHP version: ".PHP_VERSION."\n";
mysql_connect('servername','username','password');
mysql_select_db("test");
mysql_query("SET NAMES GBK");
$_POST['username'] = chr(0xbf).chr(0x27).' OR username = username /*';
$_POST['password'] = 'guess';
$username = addslashes($_POST['username']);
$password = addslashes($_POST['password']);
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysql_query($sql) or trigger_error(mysql_error().$sql);
var_dump(mysql_num_rows($result));
var_dump(mysql_client_encoding());
$username = mysql_real_escape_string($_POST['username']);
$password = mysql_real_escape_string($_POST['password']);
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysql_query($sql) or trigger_error(mysql_error().$sql);
var_dump(mysql_num_rows($result));
var_dump(mysql_client_encoding());
mysql_set_charset("GBK");
$username = mysql_real_escape_string($_POST['username']);
$password = mysql_real_escape_string($_POST['password']);
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysql_query($sql) or trigger_error(mysql_error().$sql);
var_dump(mysql_num_rows($result));
var_dump(mysql_client_encoding());</pre>
結(jié)果:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word;">PHP version: 5.2.5
int(3)
string(6) "latin1"
int(3)
string(6) "latin1"
int(0)
string(3) "gbk"</pre>
可以看出來不論是使用addslashes還是mysql_real_escape_string,我都可以利用編碼的漏洞來實現(xiàn)輸入任意密碼就能登錄服務器的注入攻擊!5熘А2霰病!(攻擊的原理我就不多說了倍谜,感興趣的同學可以研究下字符編碼中單字節(jié)和多字節(jié)的問題)
注意:第三個mysql_real_escape_string之所以能夠防注入是因為mysql_escape_string本身并沒辦法判斷當前的編碼迈螟,必須同時指定服務端的編碼和客戶端的編碼,加上就能防編碼問題的注入了尔崔。雖然是可以一定程度上防止SQL注入答毫,但還是建議以下的完美解決方案。
完美解決方案就是使用擁有Prepared Statement機制的PDO和MYSQLi來代替mysql_query(注:mysql_query自 PHP 5.5.0 起已廢棄您旁,并在將來會被移除):
PDO:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word;">$pdo = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
$stmt->execute(array('name' => $name));
foreach ($stmt as $row) {
// do something with $row
}</pre>
MYSQLi:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word;">$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
// do something with $row
}</pre>