ThinkPHP3.2应用安全

Published on 2016 - 11 - 28

在项目开发完成准备部署之前,应该检查下是否存在安全隐患,这一部分内容帮助你一起来加强项目的安全问题,指导你如何使用表单令牌、字段类型验证、输入过滤、上传安全、防止XSS攻击和目录安全保护等功能。

输入过滤

永远不要相信客户端提交的数据,所以对于输入数据的过滤势在必行,我们建议:

  • 开启令牌验证避免数据的重复提交;
  • 使用自动验证和自动完成机制进行初步过滤;
  • 使用系统提供的I函数获取用户输入数据;
  • 对不同的应用需求设置不同的安全过滤函数,常见的安全过滤函数包括stripslashes、htmlentities、htmlspecialchars和strip_tags等;

使用I函数过滤

使用系统内置的I函数是避免输入数据出现安全隐患的重要手段,I函数默认的过滤方法是htmlspecialchars,如果我们需要采用其他的方法进行安全过滤,有两种方式:

如果是全局的过滤方法,那么可以设置DEFAULT_FILTER,例如:

'DEFAULT_FILTER'        =>  'strip_tags',

设置了DEFAULT_FILTER后,所有的I函数调用默认都会使用strip_tags进行过滤。

当然,我们也可以设置多个过滤方法,例如:

'DEFAULT_FILTER'        =>  'strip_tags,stripslashes',

如果是仅需要对个别数据采用特殊的过滤方法,可以在调用I函数的时候传入过滤方法,例如:

I('post.id',0,'intval'); // 用intval过滤$_POST['id']
I('get.title','','strip_tags'); // 用strip_tags过滤$_GET['title']

要尽量避免直接使用$_GET $_POST $_REQUEST等数据,这些可能会导致安全的隐患。 就算你要获取整个$_GET数据,我们也建议你使用 I('get.') 的方式。

写入数据过滤

如果你没有使用I函数进行数据过滤的话,还可以在模型的写入操作之前调用filter方法对数据进行安全过滤,例如:

$this->data($data)->filter('strip_tags')->add();

表单合法性检测

在处理表单提交的数据的时候,建议尽量采用Think\Model类提供的create方法首先进行数据创建,然后再写入数据库。

create方法在创建数据的同时,可以进行更为安全的处理操作,而且这一切让你的表单处理变得更简单。
使用create方法创建数据对象的时候,可以使用数据的合法性检测,支持两种方式:

配置insertFields 和 updateFields属性

可以分别为新增和编辑表单设置insertFields和 updateFields属性,使用create方法创建数据对象的时候,不在定义范围内的属性将直接丢弃,避免表单提交非法数据。

insertFields 和 updateFields属性的设置采用字符串(逗号分割多个字段)或者数组的方式。

设置的字段应该是实际的数据表字段,而不受字段映射的影响。例如:

namespace Home\Model;
class UserModel extends \Think\Model{
    protected $insertFields = array('account','password','nickname','email');

定义后,调用add方法写入用户数据的时候,只能写入'account','password','nickname','email'这几个字段,编辑的时候只能更新'nickname','email'两个字段。

在使用的时候,我们调用create方法的时候,会根据提交类型自动识别insertFields和updateFields属性:

D('User')->create();

直接调用field方法

如果不想定义insertFields和updateFields属性,可以在调用create方法之前直接调用field方法,例如,实现和上面的例子同样的作用: 在新增用户数据的时候,使用:

M('User')->field('account,password,nickname,email')->create();

而在更新用户数据的时候,使用:

M('User')->field('nickname,email')->create();

这里的字段也是实际的数据表字段。

field方法也可以使用数组方式。

使用字段合法性检测后,你不再需要担心用户在提交表单的时候注入非法字段数据了。

表单令牌

ThinkPHP支持表单令牌验证功能,可以有效防止表单的重复提交等安全防护。

要启用表单令牌功能,需要配置行为绑定,在应用或者模块的配置目录下面的行为定义文件tags.php中,添加:

return array(
     // 添加下面一行定义即可
     'view_filter' => array('Behavior\TokenBuild'),
    // 如果是3.2.1以上版本 需要改成
    // 'view_filter' => array('Behavior\TokenBuildBehavior'),
);

表示在view_filter标签位置执行表单令牌检测行为。

表单令牌验证相关的配置参数有:

'TOKEN_ON'      =>    true,  // 是否开启令牌验证 默认关闭
'TOKEN_NAME'    =>    '__hash__',    // 令牌验证的表单隐藏字段名称,默认为__hash__
'TOKEN_TYPE'    =>    'md5',  //令牌哈希验证规则 默认为MD5
'TOKEN_RESET'   =>    true,  //令牌验证出错后是否重置令牌 默认为true

如果开启表单令牌验证功能,系统会自动在带有表单的模板文件里面自动生成以TOKEN_NAME为名称的隐藏域,其值则是TOKEN_TYPE方式生成的哈希字符串,用于实现表单的自动令牌验证。

自动生成的隐藏域位于表单Form结束标志之前,如果希望自己控制隐藏域的位置,可以手动在表单页面添加{__TOKEN__}标识,系统会在输出模板的时候自动替换。

如果页面中存在多个表单,建议添加标识,并确保只有一个表单需要令牌验证。

如果个别页面输出不希望进行表单令牌验证,可以在控制器中的输出方法之前动态关闭表单令牌验证,例如:

C('TOKEN_ON',false);
$this->display();

模型类在创建数据对象的同时会自动进行表单令牌验证操作,如果你没有使用create方法创建数据对象的话,则需要手动调用模型的autoCheckToken方法进行表单令牌验证。如果返回false,则表示表单令牌验证错误。例如:

$User = M("User"); // 实例化User对象
 // 手动进行令牌验证
 if (!$User->autoCheckToken($_POST)){
 // 令牌验证错误
 }

防止SQL注入

对于WEB应用来说,SQL注入攻击无疑是首要防范的安全问题,系统底层对于数据安全方面本身进行了很多的处理和相应的防范机制,例如:

$User = M("User"); // 实例化User对象
$User->find($_GET["id"]); 

即便用户输入了一些恶意的id参数,系统也会强制转换成整型,避免恶意注入。这是因为,系统会对数据进行强制的数据类型检测,并且对数据来源进行数据格式转换。而且,对于字符串类型的数据,ThinkPHP都会进行escape_string处理(real_escape_string,mysql_escape_string),还支持参数绑定。

通常的安全隐患在于你的查询条件使用了字符串参数,然后其中一些变量又依赖由客户端的用户输入。

要有效的防止SQL注入问题,我们建议:

  • 查询条件尽量使用数组方式,这是更为安全的方式;
  • 如果不得已必须使用字符串查询条件,使用预处理机制;
  • 使用自动验证和自动完成机制进行针对应用的自定义过滤;
  • 如果环境允许,尽量使用PDO方式,并使用参数绑定。

查询条件预处理

where方法使用字符串条件的时候,支持预处理(安全过滤),并支持两种方式传入预处理参数,例如:

$Model->where("id=%d and username='%s' and xx='%f'",array($id,$username,$xx))->select();
// 或者
$Model->where("id=%d and username='%s' and xx='%f'",$id,$username,$xx)->select();

模型的query和execute方法 同样支持预处理机制,例如:

$model->query('select * from user where id=%d and status=%d',$id,$status);
//或者
$model->query('select * from user where id=%d and status=%d',array($id,$status));

execute方法用法同query方法。

目录安全文件

为了避免某些服务器开启了目录浏览权限后可以直接在浏览器输入URL地址查看目录,系统默认开启了目录安全文件机制,会在自动生成目录的时候生成空白的index.html文件,当然安全文件的名称可以设置,例如你想给安全文件定义为default.html可以在入口文件中添加:

define('DIR_SECURE_FILENAME', 'default.html');
define('APP_PATH','./Application/');
require './ThinkPHP/ThinkPHP.php';

还可以支持多个安全文件写入,例如你想同时写入index.html和index.htm 两个文件,以满足不同的服务器部署环境,可以这样定义:

define('DIR_SECURE_FILENAME', 'index.html,index.htm');

默认的安全文件只是写入一个空白字符串,如果需要写入其他内容,可以通过DIR_SECURE_CONTENT参数来指定,例如:

define('DIR_SECURE_CONTENT', 'deny Access!');

注意:目录安全文件仅在第一次生成模块目录的时候生成。如果是3.2.1版本以上,则可以调用代码生成,例如:

// dirs变量是要生成安全文件的目录数组
\Think\Build::buildDirSecure($dirs);

保护模板文件

因为模板文件中可能会泄露数据表的字段信息,有两种方法可以保护你的模板文件不被访问到:

第一种方式是配置.htaccess文件,针对Apache服务器而言。

把以下代码保存在模块的模板目录(默认是View)下保存存为.htaccess。

<Files *.html>
Order Allow,Deny 
Deny from all
 </Files>

如果你的模板文件后缀不是html可以将*.html改成你的模板文件的后缀。

第二种方式是针对独立的服务器,不适合虚拟主机用户。 把项目目录部署到网站WEB目录之外,这样,整个项目目录都不能直接访问,当然模板文件也保护起来了。

上传安全

网站的上传功能也是一个非常容易被攻击的入口,所以对上传功能的安全检查是尤其必要的。

系统提供的上传类Think\Upload提供了安全方面的支持,包括对文件后缀、文件类型、文件大小以及上传图片文件的合法性检查,确保你已经在上传操作中启用了这些合法性检查。

防止XSS攻击

XSS(跨站脚本攻击)可以用于窃取其他用户的Cookie信息,要避免此类问题,可以采用如下解决方案:

  • 直接过滤所有的JavaScript脚本;
  • 转义Html元字符,使用htmlentities、htmlspecialchars等函数;
  • 系统的扩展函数库提供了XSS安全过滤的remove_xss方法;
  • 新版对URL访问的一些系统变量已经做了XSS处理。

其他安全建议

下面的一些安全建议也是非常重要的:

  • 对所有公共的操作方法做必要的安全检查,防止用户通过URL直接调用;
  • 不要缓存需要用户认证的页面;
  • 对用户的上传文件,做必要的安全检查,例如上传路径和非法格式;
  • 如非必要,不要开启服务器的目录浏览权限;
  • 对于项目进行充分的测试,不要生成业务逻辑的安全隐患(这可能是最大的安全问题);
  • 最后一点,做好服务器的安全防护;

参考文档