PHP做Web开发过程中最为常遇到的两种安全问题就是XSS和SQL注入,理论上如果开发人员有较高的安全意识,在开发过程中对于外部输入的数据做合理的验证及过滤操作是可以完全避免这些安全问题的,然而严格的控制开发人员的开发能力是较难实现的,为此最好能够通过代码框架来处理好相应的数据验证和过滤流程,但如果代码并没有基于框架开发或框架中没有集成数据过滤和验证的流程,则通过一些外部的工具进行安全的检测也是一个不错的办法。
PHP的Taint扩展通过监测PHP的执行过程来发现其中没有经过处理就使用用户输入而可能导致的问题,具体可以参考官方项目地址,不过感觉真正在用的人似乎并不太多,目前只能支持PHP 5.4及以下版本和PHP 7,即PHP 5.5和5.6不能支持。
Taint和一般的PHP扩展的安装方法完全相同,执行以下过程即可
#PHP 7 wget http://pecl.php.net/get/taint-2.0.0.tgz #PHP 5.4 wget http://pecl.php.net/get/taint-1.2.2.tgz tar -xzvf taint-2.0.0.tgz phpize ./configure make && make install
默认Taint不会被开启,需要在PHP配置中增加以下配置,并且注意不要在正式的环境中使用Taint,其会一定程度影响代码的执行效率
extension=taint.so taint.enable=1
能够进行识别的XSS漏洞就是直接输出了一段外部输入的内容,即以下形式的代码
<?php $id = $_GET['id']; //... ?> <a><?php echo $id;?></a>
如果输入的内容包含html标签,就可以向页面中注入一段自定义的内容,虽然这段内容不能够进入数据库,但攻击者可以制造一个这种连接,并引导让用户点击后就可以读取到一些用户信息之类的内容(对于这类问题可以将用户关键的Cookie信息设置为Http Only)或让用户自动进行一些行为(如给好友发带有这种连接的消息进行传播),在安装了Taint的情况下,如果执行以上代码就有相应的警告提示如下
Warning: main() [echo]: Attempt to echo a string that might be tainted in /srv/http/index.php on line 4
而如果做了相关的过滤处理则就不会有相应的警告了,例如使用strip_tags或是intval等方法。
<?php $id = strip_tags($_GET['id']); //... ?> <a><?php echo $id;?></a>
但通过对内容没有起到实质过滤效果的方法后的变量依然会有相应的警告,例如以下的几种处理
<?php $id = trim($_GET['id']); echo $id; $my_id = 'my' . $_GET['id']; echo $my_id; $ids = explode(',', $_GET['ids']); echo $ids[0];
Taint对于SQL注入的检测,是其在PHP提供的执行SQL语句的函数(目前只有mysql sqlite和PDO相关的函数)执行前判断SQL语句的变量中是否使用到了没有经过处理的用户输入会有相应的提示,例如以下代码
<?php function pdo_do_query($sql, $param) { static $pdo; if (!$pdo) { $pdo = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', ''); } $stat = $pdo->prepare($sql); $stat->execute(array_values($param)); return $stat->fetchAll(); } $id = $_GET['id']; //pdo_do_query("SELECT * FROM t WHERE id > ?", array('i' => $id)); pdo_do_query("SELECT * FROM t WHERE id > {$id}", array());
使用了prepare方法时不会有警告提示,而如果在SQL语句中直接嵌入了变量则会有以下提示
Warning: pdo_do_query() [prepare]: SQL statement contains data that might be tainted in /srv/http/index.php on line 7
同时如果使用了相应的过滤方法也不会有警告提示,例如以下代码就不会有相应的警告
<?php $pdo = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', ''); $id = $pdo->quote($_GET['id']); $sql = "SELECT * FROM t WHERE id > {$id}"; $stat = $pdo->prepare($sql); $stat->execute(); $stat->fetchAll();
感觉有可能是使用的人比较少,发现Taint有几个明显的问题但却没有人指出来,首先其对PDO的SQL注入检测目前是不起作用的,其代码如下
if (strncmp("pdo", class_name, cname_len) == 0) { if (strncmp("query", fname, len) == 0 || strncmp("prepare", fname, len) == 0) { zval *sql = ZEND_CALL_ARG(ex, arg_count); if (IS_STRING == Z_TYPE_P(sql) && TAINT_POSSIBLE(Z_STR_P(sql))) { php_taint_error(fname, "SQL statement contains data that might be tainted"); } } break; }
然而经过测试发现PDO的类名实际上是大写字母,即class_name为”PDO”导致以上的判断一直都无法成立,因此会没有作用。另外mysqli没有对prepare方法进行检测,虽然此方法一般情况都是用bind_param传递参数,但也不排除可能有的SQL里也会嵌入一些参数,所以严格意义上也是需要检测的。
最后PDO还遗漏了一个exec的方法也可以执行SQL语句也没有做检测。为此以上代码可以修改为
if (strncmp("PDO", class_name, cname_len) == 0) { if (strncmp("query", fname, len) == 0 || strncmp("exec", fname, len) == 0 || strncmp("prepare", fname, len) == 0) { zval *sql = ZEND_CALL_ARG(ex, arg_count); if (IS_STRING == Z_TYPE_P(sql) && TAINT_POSSIBLE(Z_STR_P(sql))) { php_taint_error(fname, "SQL statement contains data that might be tainted"); } } break; }
以上的问题已提交给作者,希望之后的版本中能够得到修复。