查看原文
其他

PHP安全:SQL注入漏洞防护

文章来源:计算机与网络安全

SQL注入是最危险的漏洞之一,但也是最好防护的漏洞之一。本文介绍在PHP的编码中合理地使用MySQL提供的预编译进行SQL注入防护,在PHP中使用PHP数据对象扩展或MySQLi扩展连接数据库,并且对SQL语句进行预编译处理。


如果在一些项目中无法使用预编译来防止SQL注入,可以采用传统方法来验证用户的输入是否合法,严格控制输入参数的数据类型,过滤非法字符,拦截带有SQL语法的参数传入应用程序,在一定程度上提高恶意攻击者的攻击成本,但是往往容易被绕过。


1、MySQL预编译处理


一个完整的MySQL预编译处理分为编译、执行、释放三步,预编译遵循指令和数据分离的原则,可以有效地防止SQL注入的发生。


首先是编译,通过PREPARE stmt_name FROM preparable_stm来预编译一条SQL语句。


mysql>prepare test from 'insert into hacker select ?,?,?,?';

Query OK, 0 rows affected(0.00 sec)

statement prepared


通过EXECUTE stmt_name [USING @var_name [,@var_name]…]的语法来执行预编译语句。


mysql> set @name='hacker',@email='hello@ptpress.com.cn',@password='asdfghjkl',@status=1;

Query OK, 0 rows affected(0.00 sec)

mysql> execute test using @name,@email,@password,@status;

Query OK, 1 rows affected(0.01 sec)

Records:1 Duplicates: 0 Warnings: 0

mysql> select * from hacker;

+------+------+------+------+------+

|id|name|email|password|status

+------+------+------+------+------+

|1|hacker|hello@ptpress.com.cn|asdfghjkl|1

+------+------+------+------+------+

1 row in set(0.00 sec)


可以看到,数据已经被成功地插入表中。


MySQL中的预编译语句作用域是会话级,但可以通过max_prepared_stmt_count变量来控制全局最大存储的预编译语句。


mysql> set @global.max_perpared_stmt_count=1;

Query OK, 0 rows affected(0.00 sec)

mysql> perpare selecttest from 'select * from t';

ERROR 1461(42000): Can't create more than max_prepared_stmt_count statements(current value: 1)


当预编译条数达到阈值时,可以看到MySQL会报出如上所示的错误。


如果要释放一条预编译语句,则可以使用{DEALLOCATE | DROP} PREPARE stmt_name的语法进行操作。


mysql> deallocate prepare test;

Query OK, 0 rows affected(0.00 sec)


使用Wireshark抓包工具可捕获到MySQL预编译的执行过程,如图1所示。

图1  MySQL抓包


从捕获到的流量中可以看到,每次SQL执行会分两次进行。第一次先将需要编译的SQL语句发送给数据库进行编译,数据部分用占位符代替。第二次将用户数据提交给数据库执行。SQL语句不会再次进行编译,即使用户数据中包含SQL字符也会被当成数据处理,不会改变原语句的结构。


2、PHP使用MySQL的预编译处理


SQL之所以能被注入,主要原因在于它的数据和代码指令是混合的。使用数据库预编译方式进行数据库查询,不仅可以增强系统安全性,而且可以提高系统的执行效率。当一个SQL语句需要执行多次时,使用预编译语句可以减少处理时间,提高执行效率。在PHP系统中可以通过PDO模块或MySQLi模块进行SQL预编译处理,下面依次举例说明使用方式。


(1)PDO的预编译处理举例


<?php

$dns='mysql:dbname=safe;host=127.0.0.1';

$user='root';

$password='123456';

try {

$pdo=new PDO($dns,$user,$password);

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,false);

$pdo->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);

}

catch(PDOException $e)

{

echo $e->getMessage();

}

$pdo->query("set names utf8");

$sql='insert into hacker(name,email) values(:name,:email)';

// 编译SQL

$pdo_stmt=$pdo->prepare($sql);

$name="hacker attack";

$email="safe@ptpress.com.cn";

// 绑定参数

$pdo_stmt->bindParam(':name',$name);

$pdo_stmt->bindParam(':email',$email);

$pdo_stmt->execute();

if($pdo_stmt->errorCode()==0)

{

echo "数据插入成功";

}

else

{

print_r($pdo_stmt->errorInfo());

}


在默认情况下,使用PDO并没有让MySQL数据库执行真正的预处理语句。为了解决这个问题,应该禁止PDO模拟预处理语句,添加PDO::ATTR_EMULATE_PREPARES、PDO::ATTR_ERRMODE属性。


$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,false);

$pdo->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);


(2)MySQLi的预编译处理举例


<?php

$mysqli=new mysqli("localhost","root","","safe");

if(mysqli_connect_errno())

{

printf("Connect failed: %s\n",mysqli_connect_error());

exit();

}

$mysqli->query("set names utf8");

$sql='insert into author(name,email)values(?,?)';

$mysqli_stmt=$mysqli->prepare($sql);

$mysqli_stmt->bind_param('ss',$name,$email);

$name="hacker attack";

$email="safe@ptpress.com.cn";

$res=$mysqli_stmt->execute();

if(!$res)

{

echo '错误:'.$mysqli_stmt->error;

}

else

{

echo "数据插入成功";

}

$mysqli_stmt->close();

$mysqli->close();


由于预处理是先提交SQL语句到MySQL服务端,执行预编译,客户端需要执行SQL语句时只需上传输入参数,分离了参数与SQL语句,因此不会导致恶意参数的执行,从根本上保障了数据库的安全。


3、校验和过滤


为了有效防止SQL注入,应尽量使用MySQL的预编译处理,不要使用动态拼装SQL。如果既有的系统中已经存在一些历史代码动态拼装SQL的情况,并且业务逻辑复杂,不能及时地更改为预编译处理形式,或者存在PHP版本较低、数据库版本比较老的情况,不支持预编译处理,为了防止前文提到的普通注入、隐式类型注入、盲注、二次解码注入,需要对输入的数据进行有效的校验和过滤。


通常使用的校验方式是判断传入的数据类型是否合法,如果不是所需要的要及时中断程序,防止继续执行。下面的示例中对传入的数据类型进行判定。


<?php

$id=$_GET['id'];

if(empty($id))

{

die('参数不能为空,请重新输入!');

}

if(gettype($id)!='integer')

{

die('非法的数据类型,请重新输入!');

}

if($id<=0)

{

die('输入的数据超出范围内,请重新输入!');

}


表1所列是一些常用的校验变量函数,这些函数通常用于校验用户传入的参数。

表1  常用的校验变量函数


除了上面的函数以外,也可以使用正则过滤SQL语句中的非法字符防止发生部分SQL注入方式,下面是代码示例。


<?php

function removeSpecialChar($param)

{

$regex="/\/|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_/\+|\{|\}|\:|\<|\>|\?|\[|\]|\,|\.|\/|\;|\'|\'|\-|\=|\\\|\|/";

return preg_replace($regex,"",$param);

}

$name="name' OR 'a'='a'";

$name=removeSpecialChar($name);

?>


同时还可以检查参数中是否包含SQL关键字,下面是示例代码。


<?php

eregi('select|insert|update|delete|drop|truncte|'|/*|*|../|./|union|into|load_file|outfile|union',$name);

?>


这些过滤方式都需要在特定的业务场景下使用,使用不当可能会影响到现有业务。要从根本上杜绝SQL注入漏洞,建议使用SQL预编译处理进行系统研发。


4、宽字节注入防护


要防止这类整型的宽字节注入,可以在进行SQL查询前使用intval对变量进行强制转换。


可以使用mysql_real_escape_string进行防御,在使用前需要mysql_set_charset指定当前所使用的字符集格式才能生效。


<?php

header("Content-Type: text/html;charset=UTF-8");

$conn=mysql_connect('localhost','root','') or die('数据库连接失败');

mysql_query("SET NAMES 'gbk'"); // GBK编码

mysql_select_db('safe',$conn);

mysql_set_charset('gbk',$conn);

$id=isset($_GET['id']) ? mysql_real_escape_string($_GET['id']) :1;

$sql="SELECT * FROM hacker WHERE id='{$id}'";

$result=mysql_query($sql,$conn) or die(mysql_error()); // SQL出错会报错,方便观察

$row=mysql_fetch_array($result,MYSQL_ASSOC);

print_r($row);

mysql_free_result($result);

?>


还有一种方式就是将character_set_client设置为binary,在执行SQL前先执行以下代码。


mysql_query("SET character_set_connection=gbk, character_set_results= gbk,character_set_client=binary", $conn);


将character_set_client设置成二进制格式,就不存在宽字节或多字节的问题了,所有数据以二进制的形式传递,就能有效地避免宽字符注入。


5、禁用魔术引号


PHP中的魔术引号选项magic_quotes_gpc推荐关闭,它并不能有效地防止SQL注入,已知已经有若干种方法可以绕过它,甚至由于它的存在反而衍生出一些新的安全问题。XSS、SQL注入等漏洞,都应该由应用在正确的方法中解决,同时关闭魔术引号还能提高性能。


magic_quotes_gpc=Off ; 关闭魔术引号选项




推荐阅读




*在CentOS7上安装RocketMQ 4.7.1

*PostgreSQL 提权漏洞(CVE-2018-1058)

*Node.js 目录穿越漏洞(CVE-2017-14849)

                                                                                         

                                                                        


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存