ThinkPHP安全开发规范

 

常见安全问题

目前ThinkPHP在国内中小型开发场景非常流行,但由于漏洞频发,主要集中在SQL注入、信息泄露(debug模式打开)、越权等漏洞,使得业务安全性受到不小的挑战。另外由于ThinkPHP版本比较多,实际业务多用3.2.3或5.1,因此下面主要从这两个版本来介绍ThinkPHP开发过程中常见的安全问题。

SQL注入

极少业务出现使用官方默认数据库操作方法引发SQL注入的,通常是业务不用官方I函数或者标准方法,而是自定义了过滤函数,例如下面的recursive(),由于采用的黑名单方式过滤不完整且没有对过滤结果二次验证,导致通过双写绕过:

function recursive($arr){
foreach ($arr as $k => $v) {
if (is_array($v)) {
$arr[$k]=recursive($v);
} else {
$keyword = 'select|insert|update|delete|union|into|load_file|outfile|sleep| or ';
$arr1 = explode( '|', $keyword );
$v = str_ireplace( $arr1, '', $v );
$arr[$k] = $v;
}
}
return $arr;
}

开启debug模式

外网环境开启debug模式调试,导致报错信息泄露,之前有开发认为开启error_report(0)可以避免信息泄露,然而这个处理方式对ThinkPHP是没用的。

APP_DEBUG => TRUE;

开启该选项后,一旦sql执行出错或者找不到路由,ThinkPHP则将报错路径甚至sql语句全部暴露。

越权

发生越权的情况比较普遍在于操作数据库时没有验证或者合理验证当前用户是否有权限操作,表现为sql操作时没有加上and user_id=session->userid这样的限制或者使用了and user_id=$_GET("user_id")这样的查询方式,前者的问题时没有绑定当前用户,后者的问题时没有从session获取用户身份而从用户可控参数引用。

 

安全规范

针对以上常见的安全漏洞以及ThinkPHP一年爆几次漏洞的现状,建议按照以下规范合理使用。

部署

  • 务必把你的WEB根目录指向public目录而不是应用根目录,并且不要随意更改入口文件的位置。public目录下面不要放除了入口文件和资源文件以外的其它应用文件。

关闭调试模式

  • 无论是本地开发还是生产环境部署,都不建议直接通过修改配置文件的方式开启/关闭调试模式,而应该使用环境变量(本地开发可以通过定义.env文件)。

请求变量过滤

  • 对于ThinkPHP5,框架建议的获取请求变量的方法是Request类的param方法(如非必要不要再使用get或者post方法获取,更不要使用原生的$_GET/$_POST等方法获取)。对于ThinkPHP3,框架建议在引入请求变量前先使用I函数进行过滤。然而,I函数的过滤并不完整,如果用默认I函数过滤的参数直接拼接到sql语句,大概率还是存在sql注入。
    //DEFAULT_FILTER为空
    $filters    =   isset($filter)?$filter:C('DEFAULT_FILTER');
    ...
    if(is_array($filters)){
      foreach($filters as $filter){
          if(function_exists($filter)) {
              $data   =   is_array($data) ? array_map_recursive($filter,$data) : $filter($data); // 参数过滤
          }else{
                    $data   =   filter_var($data,is_int($filter) ? $filter : filter_id($filter));
                    if(false === $data) {
                        return   isset($default) ? $default : null;
                    }
               }
          }
      }
    }
    ...
    is_array($data) && array_walk_recursive($data,'think_filter');
    ...
    //think_filter过滤也很有限
    function think_filter(&$value){
      // TODO 其他安全过滤
    
      // 过滤查询特殊字符
      if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
          $value .= ' ';
      }
    }
    
  • 对于ThinkPHP5,对于有明确类型的请求变量,可以在使用param方法的时候使用类型强制转换。
  • 对于ThinkPHP5,如果需要获取多个数据,建议使用only方法指定需要获取的变量名称,避免有些不怀好意的数据提交导致权限问题。
  • 对于ThinkPHP5,当你使用数据库或者模型操作写入数据的时候,也可以指定字段,避免非法和不希望的字段写入数据库。

上传检测

  • 系统的think\File类提供了文件上传的安全支持,包括对文件后缀、文件类型、文件大小以及上传图片文件的合法性检查。

SQL注入

  • default_filter过滤规则(默认没有任何过滤规则)
  • ThinkPHP的查询统一使用了PDO的prepare预查询和参数绑定机制,能有效的避免SQL注入的发生。但不代表绝对安全,如果你缺乏良好的代码规范,仍然有可能被利用。
  • 一般使用官方提供的标准数据库操作函数即可
    例如ThinkPHP3:
    //标准方式
    $User = M("User"); // 实例化User对象
    $data = $User->where('status=1 AND name="thinkphp"')->find();//如果where条件使用拼接参数则仍存在sql注入
    //原生方式
    $Model = new \Think\Model() // 实例化一个model对象 没有对应任何数据表
    $Model->query("select * from think_user where status=1");
    

例如ThinkPHP5:

//标准方式
Db::table('think_user')->where('id',1)->find();
//原生方式
Db::query("select * from think_user where id=? AND status=?", [8, 1]);
Db::execute("update think_user set name=:name where status=:status", ['name' => 'thinkphp', 'status' => 1]);
  • 针对ThinkPHP3的I函数需要特别说明,其默认的过滤方法是htmlspecialchars,用于xss防御是足够的,但是对于sql注入而言,则远远不够,理由上面也介绍过了。
  • 对于一些字符串的查询条件(包括原生查询)或者特殊的查询(包括Order部分),需要手动进行参数绑定,官方文档也有介绍。
    thinkphp3
    //手动绑定
    $Model = M('User');
    $where['name'] = ':name';
    $list = $Model->where($where)->bind(':name',I('name'))->select();
    //自动绑定
    $Model = M('User');
    $Model->name = 'thinkphp';
    $Model->email = 'thinkphp@qq.com';
    $Model->add();
    

    thinkphp5

    //手动绑定
    Db::query("select * from think_user where id=? AND status=?", [8, 1]);
    //自动绑定
    Db::table('think_user')
    ->where('name|title','like','thinkphp%')
    ->where('create_time&update_time','>',0)
    ->find();
    
  • 另外,如果确实需要自定义函数进行过滤,也应采用白名单的方式而不是黑名单

使用验证器

  • 对于大量的表单需要验证的情况,建议使用验证器功能统一进行数据的合规验证。验证器的验证操作应该在控制器或者路由阶段使用validate方法进行处理。

XSS攻击

  • 如果是5.1版本的话,所有的输出都已经经过了htmlentities 转义输出,确保安全。

CSRF

  • 开启表单令牌验证,尽量开启强制路由并严格规范每个URL请求,定义单独的MISS路由规则。

会话劫持

  • 在每次会话启动的时候,调用regenerate方法。
  • 开启安全头部:更改session配置参数

及时升级安全版本

  • 关注官方微信公众号或开发者周刊可及时了解
大版本 安全建议版本
3.2 3.2.4+
5.1 5.1.25+

业务逻辑安全

  • 很多漏洞源于某个业务逻辑自身的安全隐患,包括没有做合理的数据验证和权限检查,尤其是涉及资金及财务层面的,一定要做更多的安全检查,并且开启事务。
  • 一个好的建议是更多的对应用进行分层设计,减少每层的复杂性,独立的分层设计便于提高安全性。
  • 越权:自动完成规则里没有包含数据表中某个字段,遇上调用 create 方法后保存的时候就会引起越权,比如用户表中admin代表用户是否管理员,$_auto没有引入admin字段则可以越权,其他操作也类似。敏感读写操作应加上当前用户身份认证与权限判断。
    $score = ScoreOrder::where('id', $orderId)
      ->where('user_id', $this->uid)
      ->find();
    
  • 并发:对于资金或资格相关的功能,操作数据时需要加上并发锁,避免通过并发绕过限制。

 

参考

(完)