天有不测风云,事情的起因要从这次突然的内网接口安全扫描说起。这天小马突然收到大大转发的邮件,被告知某几个api存在SQL注入风险,需要立刻处理。原本安全部门是不对内网的api扫描的,可能是因为作为cgi,已经处于中间过程了,所以默认安全不扫描,自然而然程序员们也就不关注这块参数的过滤。好吧,这次人家开启内网扫描自然也是无可厚非的。于是小马排查代码后,发现大多参数都是整形参数导致的,于是用了比较简单的方式解决了,就是对参数进行整形强制类型转换。重新测试扫描,没有报注入问题了。
其实SQL注入的原理很简单,就是引号捣的鬼。这个小马在很早之前有篇文章《我不会盗QQ》,阐述得比较详细了,这里不赘述。那么写PHP该怎么防止SQL注入呢?小马整理了下,供参考。
1、关于魔术引号是否开启
在PHP.ini中设置:magic_quotes_gpc = ON。
如果设置了这个选项,那么php解析器就会自动为POST、GET、COOKIE过来的数据中所有的 '(单引号)、"(双号号)、\(反斜线)、空白字符,都为在前面自动加上转义字符"\"。
magic_quotes_gpc = Off是 php 中一种非常不安全的选项。新版本的 php 已经将默认的值改为了 On,但仍有相当多的服务器的选项为 off。有趣的是,在php5.4的更高版本中,这个选项被去掉了,即是php解析器不会自动为POST、GET、COOKIE过来的数据增加转义字符"\",而是把安全编码交给了用户自己,从而避免了magic_quotes_gpc未设置,用户依赖这个设置而带来了安全隐患。从小马的经验来看,如果默认 On对一些参数接收后的原始参数校验会有些干扰,因为毕竟有可能在原传参的基础上多了\反斜杠。
我们可以用get_magic_quotes_gpc()函数来识别当前的magic_quotes_gpc配置是否为ON,根据结果再进一步处理参数。
对这些符号,特别是引号加转义反斜杆的目的还是为了在SQL语句执行的时候,外部传参的引号(字符串)不被SQL解析成SQL的语法引号。举个例子如下:
不对单引号转义直接拼接SQL(加粗部分为外部传参):
ELECT * FROM tbl_users WHERE username='zhang3 ' OR 1 =1 UNION select cola, colb,cold FROM tbl_b #' AND password = 'abc123' LIMIT 0,1
单引号转义后拼接SQL,无法达到注入的目的:
SELECT * FROM tbl_users WHERE username='zhang3\' OR 1=1 #' AND password = 'abc123' LIMIT 0,1
总结:我们只要保持到SQL语句的变量是转义处理过的就可以了。或者说能保证经过参数校验过滤剔除强转后再拼接到SQL语句的也行,不能让参数传来的引号赤裸裸在SQL语句中。
因此,对入参万能的转义公式(不管环境中魔术引号是否开启),通常如下:
if (!get_magic_quotes_gpc())
{
if (!empty($_GET))
{
$_GET = addslashes_deep($_GET);//封装addslashes函数支持对数组参数进行递归转义。反转义函数:$str=stripcslashes($str)
}
if (!empty($_POST))
{
$_POST = addslashes_deep($_POST);
}
$_COOKIE = addslashes_deep($_COOKIE);
$_REQUEST = addslashes_deep($_REQUEST);
}
但这个万能方式有一个弊端,就是上面说的,等效于magic_quotes_gpc = ON,反斜杠会对原始传参有污染,请根据实际项目逻辑酌情添加。
2、在第一步也可以对参数进行过滤(这是适合放在入参转义的前面做的)
第一步先过传参中的滤特殊字符(如DB关键字,脚本标签,int化类型校验等),这个在阿里云对注入的处理建议中就有体现,对方直接提供了一个危险参数匹配过滤的文件,只要接进去过滤传参就可以了,不过小马见过这个过滤的代码比较长,因为匹配比较多,性能要考虑。
3、还可以用上一些DB底层的过滤转义函数,比如mysqli_real_escape_string转义特殊字符,但要注意是否存在重复转义的问题(比如已经 addslashes了就别再mysqli_real_escape_string了)。特别注意: 如果是转义符号,MySQL执行的时候会自动吃掉 比如\\\'入库是\'; \\' 入库是 \'; \'入库是'。
如果我们因为业务逻辑原因不用用上面提到的入参万能转义函数,那么就可以选择在DB底层来转义。在进行sql语句执行前,必须转义任何变量:
mysql_real_escape_string — 转义 SQL 语句中使用的字符串中的特殊字符,并考虑到连接的当前字符集。但本扩展自 PHP 5.5.0 起已废弃,并在自 PHP 7.0.0 开始被移除。应使用 MySQLi 或 PDO_MySQL 扩展 配合过滤器来替换。
4、最后入库的时候能对语句预处理就预处理,如MySQLi 和PDO(加额外的一层防止,但不能仅仅只靠预处理这一层,会有分险,别问我怎么知道的)
因为单单底层的预处理 解决不了SQL注入问题,这次的告警就是个铁证。原理大概是这样。文中说:预处理语句能很好的防止 sql注入,因为参数的发送不会影响一开始对模板的解析,编译,模版又是自己定义的,根本不会有漏洞。
如果说预处理会把注入的变量' AND 1=1 自动当一个值处理 而不会拼接成SQL语句语法,如果是这样那就对防止SQL注入有效,但为什么本次扫描还报注入漏洞呢,小马排查底层代码是有预处理的(本来也是想着依赖这一层,没想到没防住)?个人反而感觉预处理只是 对?问号的值替换,该拼接的还是会拼接,没啥防注入效果,这点需特别小心和关注。只至小马看了以上的这篇文章后,方知此预处理非彼预处理,特来更正。文章中的才是真正的预处理语法,从代码中,我们就可以看到预处理语句的两大优势的体现。首先是占位符,使用占位符之后,我们就不用在 SQL 语句中去写单引号,单引号往往就是 SQL 注入的主要漏洞来源。bindParam() 方法会自动地转换绑定数据的类型。当然,bindParam() 方法也可以在可选的参数中指定绑定的数据类型,这样就能让我们的代码更加安全了,大家可以查阅相关的文档。而小马使用的预处理只是底层的预处理语法并没有按预处理的语法处理传参,SQL语句中的参数还是拼接的,只是用了sprintf替换而已,所以是无效的,并不代表预处理本身无法防止SQL注入。
另一个优势就是模板的能力,我们只定义了一个 PDOStatement 对象,然后通过改变数据的内容,就可以多次地使用 execute() 方法去执行预处理语句。
总结:建议处理流程,特殊字符过滤剔除;参数校验类型转换;入库之前使用转义addslashes函数;底层预处理;SQL执行。
其他环境上的诸如,PHP配置文件中Register_globals=off; php安全模式:safe_mode=on等等环境配置上的安全策略也需要关注。
网友评论