- ·上一篇文章:7种流行PHP集成开发工具(IDE)的比较
- ·下一篇文章:在PHP中全面阻止SQL注入式攻击之二
在PHP中全面阻止SQL注入式攻击之三
一、建立一个安全抽象层
我们并不建议你手工地把前面介绍的技术应用于每一个用户输入的实例中,而是强烈推荐你为此创建一个抽象层。一个简单的抽象是把你的校验方案加入到一个函数中,并且针对用户输入的每一项调用这个函数。当然,我们还可以创建一种更复杂的更高一级的抽象-把一个安全的查询封装到一个类中,从而应用于整个应用程序。在网上已经存在许多这种现成的免费的类;在本篇中,我们正要讨论其中的一些。
进行这种抽象至少存在三个优点(而且每一个都会改进安全级别):
1.本地化代码。
2.使查询的构造更快且更为可靠-因为这可以把部分工作交由抽象代码来实现。
3.当基于安全特征进行构建并且恰当使用时,这将会有效地防止我们前面所讨论的各种各样的注入式攻击。
二、改进现有的应用程序
如果你想改进一个现有的应用程序,则使用一个简单的抽象层是最适当的。一个能够简单地"清理"你所收集的任何用户输入内容的函数可能看起来如下所示:
functionsafe($string){ return"'".mysql_real_escape_string($string)."'" } |
【注意】我们已经构建了相应于值要求的单引号以及mysql_real_escape_string()函数。接下来,就可以使用这个函数来构造一个$query变量,如下所示:
$variety=safe($_POST['variety']); $query="SELECT*FROMwinesWHEREvariety=".$variety; |
现在,你的用户试图进行一个注入式攻击-通过输入下列内容作为变量$variety的值:
|
|
注意,如果不进行上面的"清理",则最后的查询将如下所示(这将导致无法预料的结果):
SELECT*FROMwinesWHEREvariety='lagrein'or1=1;' |
然而现在,既然用户的输入已经被清理,那么查询语句就成为下面这样一种无危害的形式:
SELECT*FROMwinesWHEREvariety='lagrein\'or1=1\;' |
既然数据库中不存在与指定的值相应的variety域(这正是恶意用户所输入的内容-lagrein'or1=1;),那么,这个查询将不能返回任何结果,并且注入将会失败。
三、保护一个新的应用程序
如果你正在创建一个新的应用程序,那么,你可以从头开始创建一个安全抽象层。如今,PHP5新改进的对于MySQL的支持(这主要体现在新的mysqli扩展中)为这种安全特征提供了强有力的支持(既有过程性的,也有面向对象特征的)。你可以从站点http://php.net/mysqli上获取有关mysqli的信息。注意,只有当你使用--with-mysqli=path/to/mysql_config选项编译PHP时,这种mysqli支持才可用。下面是该代码的一个过程性版本,用于保护一个基于mysqli的查询:
<?php //检索用户的输入 $animalName=$_POST['animalName']; //连接到数据库 $connect=mysqli_connect('localhost','username','password','database'); if(!$connect)exit('connectionfailed:'.mysqli_connect_error()); //创建一个查询语句源 $stmt=mysqli_prepare($connect,"SELECTintelligenceFROManimalsWHEREname=?"); if($stmt){ //把替代绑定到语句上 mysqli_stmt_bind_param($stmt,"s",$animalName); //执行该语句 mysqli_stmt_execute($stmt); //检索结果... mysqli_stmt_bind_result($stmt,$intelligence); //...并显示它 if(mysqli_stmt_fetch($stmt)){ print"A$animalNamehas$intelligenceintelligence.\n"; }else{ print'Sorry,norecordsfound.'; } //清除语句源 mysqli_stmt_close($stmt); } mysqli_close($connect); ?> |
该mysqli扩展提供了一组函数用于构造和执行查询。而且,它也非常准确地提供了前面使用我们自己的safe()函数所实现的功能。
在上面的片断中,首先收集用户提交的输入内容并建立数据库连接。然后,使用mysqli_prepare()函数创建一个查询语句源-在此命名为$stmt以反映使用它的函数的名称。这个函数使用了两个参数:连接资源和一个字符串(每当你使用扩展插入一个值时,"?"标记被插入到其中)。在本例中,你仅有一个这样的值-动物的名字。
注意,在一个SELECT语句中,放置"?"标记的唯一的有效位置是在值比较部分。这正是为什么你不需要指定使用哪个变量的原因(除了在mysqli_stmt_bind_param()函数中之外)。在此,你还需要指定它的类型-在本例中,"s"代表字符串。其它可能的类型有:"I"代表整数,"d"代表双精度数(或浮点数),而"b"代表二进制字符串。
函数mysqli_stmt_execute(),mysqli_stmt_bind_result()和mysqli_stmt_fetch()负责执行查询并检索结果。如果存在检索结果,则显示它们;如果不存在结果,则显示一条无害的消息。最后,你需要关闭$stmt资源以及数据库连接-从内存中对它们加以释放。
假定一个合法的用户输入了字符串"lemming",那么这个例程将(假定是数据库中适当的数据)输出消息"Alemminghasverylowintelligence."。假定存在一个尝试性注入-例如"lemming'or1=1;",那么这个例程将打印(无害)消息"Sorry,norecordsfound."。
此外,mysqli扩展还提供了一个面向对象版本的相同的例程。下面,我们想说明这种版本的使用方法。
<?php $animalName=$_POST['animalName']; $mysqli=newmysqli('localhost','username','password','database'); if(!$mysqli)exit('connectionfailed:'.mysqli_connect_error()); $stmt=$mysqli->prepare("SELECTintelligence FROManimalsWHEREname=?"); if($stmt){ $stmt->bind_param("s",$animalName); $stmt->execute(); $stmt->bind_result($intelligence); if($stmt->fetch()){ print"A$animalNamehas$intelligenceintelligence.\n"; }else{ print'Sorry,norecordsfound.'; } $stmt->close(); } $mysqli->close(); ?> |
实际上,这部分代码是前面描述代码的复制-它使用了一种面向对象的语法和组织方法,而不是严格的过程式代码。
四、更高级的抽象
如果你使用外部库PearDB,那么,你可以对应用程序的安全保护模块进行全面的抽象。
另一方面,使用这个库存在一个突出的缺点:你只能受限于某些人的思想,而且代码管理方面也添加了大量的工作。为此,在决定是否使用它们之前,你需要进行仔细地斟酌。如果你决定这样做,那么,你至少确保它们能够真正帮助你"清理"你的用户输入的内容。
五、测试你的注入式保护能力
正如我们在前面所讨论的,确保你的脚
