PHP 安全:过滤、验证和转义

咱们在开发应用时,通常有个约定:不要信任任何来自不受本身控制的数据源中的数据。例如如下这些外部源:php

  • $_GET
  • $_POST
  • $_REQUEST
  • $_COOKIE
  • $argv
  • php://stdin
  • php://input
  • file_get_contents()
  • 远程数据库
  • 远程API
  • 来自客户端的数据

全部这些外部源均可能是攻击媒介,可能会(有意或无心)把恶意数据注入PHP脚本。编写接收用户输入而后渲染输出的PHP脚本很容易,但是要安全实现的话,须要下一番功夫。我这里以陈咬金的三板斧为引子,给你们介绍三招:过滤输入、验证数据,以及转义输出。html

一、过滤输入

过滤输入是指转义或删除不安全的字符。在数据到达应用的存储层以前,必定要过滤输入数据,这是第一道防线。假如网站的评论框接受HTML,用户能够随意在评论中加入恶意的<script>标签,以下所示:laravel

<p>
这篇文章颇有用!
</p>
<script>windows.location.href="http://laravelacademy.org";</script>

若是不过滤这个评论,恶意代码会存入数据库,而后在网页中渲染,当用户访问这个页面时,会重定向到可能不安全的钓鱼网站(这种攻击有一个更专业的称呼:XSS攻击)。这个简单示例很好的说明了为何咱们要过滤不受本身控制的输入数据。一般咱们要过滤的输入数据包括HTML、SQL查询以及用户资料等。程序员

HTML

咱们可使用PHP提供的htmlentities函数过滤HTML,该函数会将全部HTML标签字符(&、<、>等)转化为对应的HTML实体,以便在应用存储层取出后安全渲染。可是有时候咱们是容许用户输入某些HTML元素的,尤为是输入富文本的时候,好比图片、连接这些,可是htmlentities不能验证HTML,检测不出输入字符串的字符集,故而没法实现这样的功能。正则表达式

<?php
$input = "<p><script>alert('Laravel学院');</script></p>";
echo htmlentities($input, ENT_QUOTES, 'UTF-8');

htmlentities的第一个参数表示要处理的HTML字符串,第二个参数表示要转义单引号,第三个参数表示输入字符串的字符集编码。sql

htmlentities相对的是html_entity_decode方法,该方法会将全部HTML实体转化为对应的HTML标签。数据库

此外,PHP还提供了一个相似的内置函数htmlspecialchars,该函数也是用于将HTML标签字符转化为HTML实体,只是可以转化的字符有限(参考官方文档:http://php.net/manual/zh/function.htmlspecialchars.php),若是要转化全部字符仍是使用htmlentities方法,值得一提的是和htmlentities同样,htmlspecialchars也有一个与之相对的方法htmlspecialchars_decodewindows

若是想要直接将输入字符串中的全部HTML标签去掉,可使用strip_tags方法。数组

若是须要更增强大的过滤HTML功能,可使用HTML Purifier库,这是一个很强健且安全的PHP库,专门用于使用指定规则过滤HTML输入。在Laravel中咱们可使用相应的扩展包来实现过滤功能:http://laravelacademy.org/post/3914.html安全

SQL查询

有时候应用必须根据输入数据构建SQL查询,这些数据可能来自HTTP请求的查询字符串,也可能来自HTTP请求的URI片断,一不当心,就有可能被不怀好意的人利用进行SQL注入攻击(拼接SQL语句对数据库进行破坏或者获取敏感信息)。不少初级的程序员可能会这么写代码:

$sql = sprintf(
    'UPDATE users SET password = "%s" WHERE id = %s',
    $_POST['password'],
    $_GET['id']
);

这么作风险很大,好比某我的经过以下方式对HTTP发送请求:

POST /user?id=1 HTTP/1.1
Content-Length: 17
Content-Type: application/x-www-form-urlencoded

password=abc”;--

这个HTTP请求会把每一个用户的密码都设置为abc,由于不少SQL数据库把—视做注释的开头,因此会忽略后续文本。

在SQL查询中必定不能使用未过滤的输入数据,若是要在SQL查询中使用输入数据,必定要使用PDO预处理语句(PDO是PHP内置的数据库抽象层,为不一样的数据库驱动提供统一接口),PDO预处理语句是PDO提供的一个功能,能够用于过滤外部数据,而后把过滤后的数据嵌入SQL语句,避免出现上述SQL注入问题,此外预处理语句一次编译屡次运行,能够有效减小对系统资源的占用,获取更高的执行效率。关于PDO后咱们后续还会在数据库部分重点讨论。

值得注意的是,不少现代PHP框架都使用了MVC架构模式,将数据库的操做封装到了Model层,框架底层已经作好了对SQL注入的规避,只要咱们使用模型类提供的方法执行对数据库的操做,基本上能够避免SQL注入风险。

咱们以Laravel为例看看底层是如何规避SQL注入的,改写上面的update语句,代码会是这样:

$id = $_GET['id'];
$password = $_POST['password'];
User::find($id)->update(['password'=>bcrypt($password)]);

因为模型类底层调用的是是查询构建器的方法,因此最终会调用Builder(Illuminate\Database\Query\Builder)的update方法:

public function update(array $values)
{
    $bindings = array_values(array_merge($values, $this->getBindings()));

    $sql = $this->grammar->compileUpdate($this, $values);

    return $this->connection->update($sql, $this->cleanBindings($bindings));
}

这段代码传入参数是要更新的值,而后经过$bindings得到绑定关系,这里咱们咱们获取到的应该是包含passwordupdated_at(默认更新时间戳)的数组,而后再经过Grammar(Illuminate\Database\Query\Grammars\Grammar)类的compileUpdate方法生成预处理SQL语句,这里对应的sql语句是:

update `users` set `password` = ?, `updated_at` = ? where `id` = ?

而后最终将预处理sql语句和对应绑定关系传递给数据库去执行。关于SQL注入咱们还会在后续数据库部分继续讨论。

用户资料信息

若是应用中有用户帐户,可能就要处理电子邮件地址、电话号码、邮政编码等资料信息。PHP预料到会出现这种状况,所以提供了filter_varfilter_input函数。这两个函数的参数能使用不一样的标志,过滤不一样类型的输入:电子邮件地址、URL编码字符串、整数、浮点数、HTML字符、URL和特定范围的ASCII字符。

如下示例展现了如何过滤电子邮件地址,删除除字母、数字和!#$%&'*+-/=?^_{|}~@.[]`以外的全部其余字符:

<?php
$email = 'yaojinbu@163.com';
$emailSafe = filter_var($email, FILTER_SANITIZE_EMAIL);

更多filter_var的使用请参考PHP官方文档:http://php.net/manual/zh/function.filter-var.php,相应的移除过滤器请参考:http://php.net/manual/zh/filter.filters.sanitize.php

固然,filter_var函数还能够用于其它表单提交数据的过滤。

 

二、验证数据

PHP原生实现

 

验证输入数据也很重要,与过滤不一样,验证不会从输入数据中删除信息,而只是确认用户输入是否符合预期。若是输入的是电子邮件地址,则确保用户输入的是电子邮件地址;若是须要的是电话号码,则确保用户输入的是电话号码,这就是验证要作的事儿。

验证是为了保证在应用的存储层保存符合特定格式的正确数据,若是遇到无效数据,要停止数据存储操做,并显示相应的错误信息来提醒用户输入正确的数据。验证还能避免数据库出现潜在错误,例如,若是MySQL指望使用DATETIME类型的值,而提供的倒是DATE字符串,那么MySQL会报错或使用默认值,无论哪一种处理方式,应用的完整性都受到无效数据的破坏。

要实现输入数据验证,咱们能够把某个FILTER_VALIDATE_*标识传递给filter_var函数,PHP提供了验证布尔值、电子邮件地址、浮点数、整数、IP、正则表达式和URL的标识(详见http://php.net/manual/en/filter.filters.validate.php)。下面的示例演示了如何验证电子邮件地址:

<?php
$input = 'yaojinbu@163.com';
$isEmail = filter_var($input, FILTER_VALIDATE_EMAIL);
if ($isEmail !== FALSE) {
    echo 'success';
} else {
    echo 'failed';
}

咱们要特别关注filter_var的返回值,若是验证成功,返回的是要验证的值,若是验证失败,返回false

借助PHP组件

虽然filter_var函数提供了不少用于验证的标识,但一招鲜,不能吃遍天,咱们不能依赖它验证全部数据,除了filter_var函数,还有如下组件能够帮助咱们完成更加复杂的验证功能:

注:输入数据既要验证也要过滤,以确保其符合预期且安全

三、PHP 转义实现

把输出渲染成网页或API响应时,必定要转义输出,这也是一种防御措施,能避免渲染恶意代码,形成XSS攻击,还能防止应用的用户无心中执行恶意代码。

咱们可使用前面提到的htmlentities函数转移输出,该函数的第二个参数必定要使用ENT_QUOTES,让这个函数转义单引号和双引号,并且,还要在第三个参数中指定合适的字符编码(一般是UTF-8),下面的例子演示了如何在渲染前转义HTML输出:

<?php
$output = '<p><script>alert(“欢迎来到Laravel学院!")</script></p>';
echo htmlentities($output, ENT_QUOTES, ‘UTF-8');

若是不转义直接输出,会弹出提示框:

alert

转义以后输出变成:

<p><script>alert("欢迎访问Laravel学院!");</script></p>

现代PHP支持许多模板引擎,这些模板引擎在底层已经为了作好了转义处理,好比如今流行的twig/twigsmarty/smarty都会自动转义输出。这种默认处理方式很赞,为PHP Web应用提供了有力的安全保障。

相关文章
相关标签/搜索