CodeIgniter 框架阅读

对web框架的安全,我主要关注了以下方面。

  • 开发者如何拿到输入,框架对输入的处理方式
  • 框架如何处理路由
  • 框架如何封装数据库查询
  • 框架对常见操作的处理(session,文件上传)

阅读的思路是先看整体框架的结构,然后通过看官方文档的描述然后找到对应代码阅读。

输入类

输入类有两个用途:

  • 为了安全性,对输入数据进行预处理
  • 提供了一些辅助方法来获取输入数据并处理

安全性过滤
当访问 控制器 时,安全过滤方法会自动被调用, 它做了以下几件事情:

  • 如果 $config['allow_get_array'] 设置为 FALSE (默认是 TRUE),销毁全局的 GET 数组。
  • 当开启 register_globals 时,销毁所有的全局变量。
  • 过滤 GET/POST/COOKIE 数据的键值,只允许出现字母和数字(和其他一些)字符。
  • 提供了 XSS (跨站脚本攻击)过滤,可全局启用,或按需启用。
  • 将换行符统一为 PHP_EOL (基于 UNIX 的系统下为 \n,Windows 系统下为 \r\n),这个是可配置的。

XSS 过滤
输入类可以自动的对输入数据进行过滤,来阻止跨站脚本攻击。如果你希望在每次遇到 POST 或 COOKIE 数据时自动运行过滤,你可以在 application/config/config.php 配置文件中设置如下参数:
$config['global_xss_filtering'] = TRUE;

首先CI会对$_GET$_POST$_COOKIE的key和value进行处理,对应代码为$this->_sanitize_globals();
对key处理的正则是/^[a-z0-9:_\/|-]+$/i,即只允许字母和数字,波浪符(~),百分号(%),句号(.),分号(:),下划线(_),连字号(-),空格。在key中一旦出现了非法字符就会exit。

对value用remove_invisible_characters进行处理。该函数会把无法打印的字符替换成空。无法打印的字符定义如下。
$non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127

由此可见,可以在value中插入一些不可见字符,而不影响value的结果。这个性质可以用来bypass一些流量过滤型的waf。

输入类里还定义了一个is_ajax_request()的函数用于判断是否是ajax请求,判断方式是strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'

参考页面

http://codeigniter.org.cn/user_guide/libraries/input.html
http://codeigniter.org.cn/user_guide/general/security.html

路由类

URI 分段

example.com/class/function/ID

  • 第一段表示要调用的控制器 类 ;
  • 第二段表示要调用的类中的 函数 或 方法 ;
  • 第三段以及后面的段代表传给控制器的参数,如 ID 或其他任何变量,可以传递多个参数;

即通过URL就能访问到控制器中的某个函数并能提供String类型的参数。
如果启用了查询字符串( application/config.php 配置文件中$config['enable_query_strings'] = TRUE;,默认关闭),那么可以输入数组类型的参数。

其中不能通过URL直接访问到的私有方法的标志是方法声明为 private 或 protected 或者方法名前加上一个下划线前缀。

CI框架会对URI的每个segment进行过滤,$config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-';。如果URI里有非法字符会直接400。

URI 路由

路由规则定义在 application/config/routes.php 文件中,在这个文件中你会 发现一个名为 $route 的数组,利用它你可以设置你自己的路由规则。 在路由规则中你可以使用通配符或正则表达式。

$route['product/:num'] = 'catalog/product_lookup';
自定义的路由可以用正则或者通配符,对应处理的函数也可以动态生成。

在路由中使用 HTTP 动词$route['products']['put'] = 'product/insert';

参考页面

http://codeigniter.org.cn/user_guide/general/urls.html
http://codeigniter.org.cn/user_guide/general/routing.html

数据库查询

CI框架在进行数据库查询的时候没有对表名和列名进行处理,凡是能控制表名或者列名的地方都能引发注入。
在一些传输数组进行查询的地方,如$this->db->where($array);或者$this->db->like($array);,有可能从输入中引入表名。但是由于对数组的key的检查,导致很难通过注入列名的方式来攻击。
因此,对于CI框架,SQL注入主要发生在字符串拼接语句并带入查询的时候。

如果存在反序列化漏洞,可能可以通过找一个能__toString的类来实例化,绕过对输入的escape。CI框架本身的类里不存在__toString方法。

参考页面

http://codeigniter.org.cn/user_guide/database/index.html

上传类

CI的上传类会对上传后文件的原始文件名进行sanitize_filename处理,而sanitize_filename函数没有对windows下目录分隔符为\的情况进行处理,导致文件名即使经过sanitize_filename过滤,在windows下用..\..切换目录仍然可行。
即上传的文件原始文件名如果包含..\..则保存的文件可能逃逸当前目录。
但是实际测试发现,php会对上传后的文件名进行取basename的操作,导致不能出现..\。所以在此处并不能被利用。
php对此这样处理的原因是在IE浏览器中,上传文件会附带文件的全路径。根据php代码里的注释,取basename的操作可能在以后版本中取消。

参考页面

http://codeigniter.org.cn/user_guide/libraries/file_uploading.html
https://github.com/php/php-src/blob/6053987bc27e8dede37f437193a5cad448f99bce/main/rfc1867.c#L1156

杂七杂八

CI框架提供了hook的功能。

  • pre_system 在系统执行的早期调用,这个时候只有 基准测试类 和 钩子类 被加载了, 还没有执行到路由或其他的流程。
  • pre_controller 在你的控制器调用之前执行,所有的基础类都已加载,路由和安全检查也已经完成。
  • post_controller_constructor 在你的控制器实例化之后立即执行,控制器的任何方法都还尚未调用。
  • post_controller 在你的控制器完全运行结束时执行。
  • display_override 覆盖 _display() 方法,该方法用于在系统执行结束时向浏览器发送最终的页面结果。 这可以让你有自己的显示页面的方法。注意你可能需要使用 $this->CI =& get_instance() 方法来获取 CI 超级对象,以及使用 $this->CI->output->get_output() 方法来 获取最终的显示数据。
  • cache_override 使用你自己的方法来替代 输出类 中的 _display_cache() 方法,这让你有自己的缓存显示机制。
  • post_system 在最终的页面发送到浏览器之后、在系统的最后期被调用。

在测试的时候做一些hook进行一些比较高级的操作。比如可以通过display_override来获取页面的输出,检测页面上可能出现的敏感的信息。

参考页面

http://codeigniter.org.cn/user_guide/general/hooks.html

总结

CI框架的代码整体看下来安全性较高,由于对用户的输入做了一些过滤,导致很多可能出现的问题难以被利用。
对于一些没有考虑到的问题,还希望能与大佬们交流。

分享到 评论