注入攻击作为OWASP top10绝对的No.1被广为人知,其中SQL注入首当其冲,除了转义过滤拦截敏感字符之外,安全从业者较为推荐的防止SQL注入的方法是使用预编译。那么预编译能够防止SQL注入的根本原因是什么?它真的能够完全防止SQL注入吗?本文从PHP的PDO和MySQL入手,对以上两个问题进行探究。
PDO是如何工作的
PDO(PHP Data Object),PHP数据对象,它提供了一个轻量级的接口,使得我们不管使用哪种数据库,都可以用相同的函数(方法)来查询和获取数据,同时,它的预编译机制可以用来防止SQL注入。PDO有两种模式:本地预处理和模拟预处理。
模拟预处理用于数据库不支持预编译机制的情况,其本质是在底层先对用户输入进行转义后,再对SQL语句进行拼接,然后将完整的语句发送给数据库执行。任何可控的拼接都是具有一定危险性的,在PHP 5.3.6前,这种转义是通过单字节字符集来完成的,因此存在宽字节注入。但在正确设置字符集的前提下,不可否认这种方式是可以防止SQL注入的。
本地预处理则是通过数据库的预编译机制来完成,是真正的预编译机制。
通过下面的代码示例,我们可以看到两种预处理方式的不同:
访问测试页,使用wireshark抓包。
可以看到,默认状态下启用了模拟预处理,整个SQL语句由PHP处理并转义,然后交由MySQL处理。
在源码中添加以下代码:
即可开启本地预处理模式。
可以看到,整个步骤分为两步,先将预处理语句发送给MySQL,再发送参数由MySQL插入并执行,整个过程不涉及转义。
至此可以得出,PDO的模拟预处理模式仍是依靠转义字符来防止SQL注入的,而本地预处理模式则是使用了真正的数据库预编译。
预编译防止注入的原因
关于预编译能够防止注入的原因,还要从预编译的运行机制说起。通常来说,在MySQL中,一条SQL语句从传入到执行经历了以下过程:检查缓存、规则验证、解析器解析为语法树、预处理器进一步验证语法树、优化SQL、生成执行计划、执行。
其中对语法的解析和优化的过程其实是与传入的字段值无关的,但却比真正执行的过程更为耗费时间,因此在处理某些语句时,很容易造成时间的浪费,效率的下降,如:
这两个语句由于id后的值不同,因此在检查缓存阶段不会匹配,不能得到重用,后面的所有阶段都要再依次进行,但它们的语法树却是相似的,只是id字段的值不同。
预编译使用占位符?代替字段值的部分,将SQL语句先交由数据库预处理,构建语法树,再传入真正的字段值多次执行,省却了重复解析和优化相同语法树的时间,提升了SQL执行的效率。
正因为在传入字段值之前,语法树已经构建完成,因此无论传入任何字段值,都无法再更改语法树的结构。至此,任何传入的值都只会被当做值来看待,不会再出现非预期的查询,这便是预编译能够防止SQL注入的根本原因。
使用预编译真的绝对安全吗
预编译的字段值在构建语法树之后传入的机制,从根本上杜绝了SQL注入发生的可能,这就如同CSP防止XSS的绝对性一样(当然这是从对抗XSS的层面上来说,CSP是可以被其他机制绕过的)。那么使用预编译真的没有发生SQL注入的可能性吗?只能说理论上是这样的,但实际使用过程中仍有一些行为可能导致风险。
首先,开发人员应该确保正确使用预编译。笔者曾在代码审计中发现有开发者虽然使用了预编译机制,但参数值仍是由加号拼接上去的,而不是正确的使用参数绑定,这样仍是可以造成SQL注入的。
其次,并不是所有参数都可预编译,如:
上面的SQL语句是错误的,因为表名和列名是不能够被预编译的,这是由于生成语法树的过程中,预处理器在进一步检查解析后的语法树时,会检查数据表和数据列是否存在,因此数据表和数据列不能被占位符?所替代。但在很多业务场景中,表名需要作为一个变量存在,因此这部分仍需由加号进行SQL语句的拼接,若表名是由外部传入且可控的,仍会造成SQL注入。
同理,ORDER BY后的ASC/DESC也不能被预编译,当业务场景涉及到用户可控制排序方式,且ASC/DESC是由前台传入并拼接到SQL语句上时,就可能出现危险了。
结语
笔者建议在程序内部创建需要用到的表名的集合,每次查询时从集合中选出,由此避免表名被篡改,ASC/DESC也应在做严格检查后再拼接,最好不要由前台传入。任何的拼接都有可能出现问题,总体来说,正确的使用预编译还是可以杜绝SQL注入攻击的。
你与世界
只差一个公众号
领取专属 10元无门槛券
私享最新 技术干货