问:
如果用户使用SQL查询语句时,进行"插入而不修改"的操作,那么网站应用将很容易遭到SQL注入攻击,例如下面这个例子:
$unsafe_variable = $_POST['user_input'];
mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");
这是因为用户可以输入下面这样的代码段 value');
DROP TABLE table;--
, 这样查询语句就变成:
INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')
我该做什么才能防止这样的攻击发生?
答:
应当使用合适的语句和参数化的查询. 这样的SQL语句会在被发送给数据库服务引擎之后解析并被分离成些许参数. 这样攻击者就不可能输入恶意的SQL语句了.
基本上你有这样两种选择:
-
使用 PDO (对于任何支持的数据库驱动):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name'); $stmt->execute(array('name' => $name)); foreach ($stmt as $row) { // do something with $row }
-
使用 MySQLi (对于 MySQL):
$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 }
如果你是连接到除Mysql以外的数据库, there is a driver-specific second option that you can refer to (e.g. pg_prepare()
and pg_execute()
for
PostgreSQL). PDO 是通用的选择.
记住当使用 PDO
去连接一个MySQL数据库时,a
MySQL database real prepared statements are not used by default. 你必须禁用已有的语句来修复它. 一个使用PDO创建连接的例子是:
$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
在上述的例子中,错误模块不是必须的,但是我们建议去添加它.这样当发生一些错误时,脚本不会抛出 Fatal
Error
而停止,他给了开发者机会去 catch
那些throw
n as PDOException
的异常.
第一行 setAttribute()
是强制的,
他告诉PDO不使用预先准备的参数语句而使用当前传入的参数. 这确保指令和语句在发送给MySQL服务之前不会被PHP解析. (不留任何注入恶意SQL的机会给可能的攻击者).
尽管你可以设置 charset
字符集作为构造函数的参数选项,
但老版的PHP(< 5.3.6)会在DSN中忽略字符集参数.
解释
What happens is that the SQL statement you pass to prepare
is
parsed and compiled by the database server. 你通过传递特殊的变量(如一个 ?
或一个已命名的变量如上述例子中的 :name
) 告诉数据库引擎你想在哪些制定的地方过滤(替换).
然后你可以调用 execute
,
预先的语句就会和你指定的参数混合在一起.
变量值是和已编译的语句混合在一起而非一个SQL字符串. SQL注入通过哄骗脚本在它创建SQL语句并传递给数据库时包含恶意字符串. 所以通过单个的变量来发送实际的SQL语句, 你就限制了导致非预期结果的风险. 任何你使用预先准备好的语句发送的变量将只会被作为字符串来对待
(尽管数据库引擎可能会做一些优化,一些变量会被作为数字来对待). 在上述例子中,如果 $name
变量包含 'Sarah';
DELETE FROM employees
,执行后将只简单地查询包含 "'Sarah';
DELETE FROM employees"
的字符串,你也不会得到一个空的表.
另一个使用预先语句的好处是,如果你在同个缓存中多次执行相同的语句,他将只被解析和编译一次, 使你获得速度上的收益.
哦, 既然你问了如何执行插入操作,这就是个例子 (使用 PDO):
$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');
$preparedStatement->execute(array('column' => $unsafeValue));
预先准备好的语句能被用于动态查询吗?
可以,但动态查询的特征可能不能被参数化.
对于这些特殊的情况,最好的方式是使用一个"白名单过滤器"来约束可能的值.
// Value whitelist
// $dir can only be 'DESC' otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
$dir = 'ASC';
}