PHP,DDD,CQRS,Event Sourcing,Kubernetes,Docker,Golang

0%

关于Yii中findByPk被sql注入的一个误区

树大招风,当然网站被恶意攻击注入是常有的事,这也是对一个程序员代码质量以及经验水平的考验。

今天公司一个项目不幸被sql注入了,根据日志查询到了注入点,是某个详情页的代码,大致就是根据某条数据id查询该条数据。

理所当然的,我们会使用Yii中Ar类提供的findByPk()方法来查询该数据,而正是这里却被注入了。

首先有人可能会想,数据的主键,不就是一个自增id数字吗?那未必,但大多数情况下其实就是自增id。

那么既然是自增id,是int型,Yii提供的这个方法不会帮我们去验证字段类型吗?Yii当然会帮我们验证该字段类型。这里推荐一下这篇文章(http://www.yiiframework.com/wiki/275/how-to-write-secure-yii-applications/),比较适合使用Yii开发的人员如何注意安全

这篇文章中有段提到:In fact, Yii will help, even in the first case. The method findByPk() uses the table schema to ensure that a numeric column only gets numeric criteria.

说明使用Yii的findByPk()会使用数据表schema确保对应类型的字段获得对应类型的查询条件,那么也就是说int型自增id字段通过这个函数传入id值,都是能验证或转换成对应的int型。可此时此刻并没有,我们还是被注入了。

那么我们就要去分析一下到底是什么原因导致了这样,难道是Yii版本的问题?我们分析一下源码。

从Ar类ActiveRecord入手,找到findByPk(),再找到了CDbCommandBuilder类中的createPkCriteria()方法,接着找到同类中的createInCondition()方法,会发现该方法中使用了一个方法typecast()将传入的id值进行了类型转换,这个方法位于CMysqlColumnSchema类中,该类的一个实例就代表着一张表中的一个字段,在这里是主键id字段,其源代码是这样的:

public function typecast($value)
{
    if(gettype($value)===$this->type || $value===null || $value instanceof CDbExpression)
        return $value;
    if($value==='' && $this->allowNull)
        return $this->type==='string' ? '' : null;
    switch($this->type)
    {
        case 'string': return (string)$value;
        case 'integer': return (integer)$value;
        case 'boolean': return (boolean)$value;
        case 'double':
        default: return $value;
    }
}

$value是传入的id值,$this->type是字段类型,假设弱类型语言php获得到的id数字是string类型,字段类型是integer,那么逻辑就会进入switch内,并且走的是case ‘integer’,将string类型转换为integer,但并没有。
打印$this->type出来看,竟然显示的是string,也就是Yii将该表中的主键id认为是string类型,也就因此没有将传入参数转换成integer型,所以被注入了。

但究竟是什么原因导致的?于是接着开始找是什么地方使得$this->type值为string的,于是找到该类的extractType()方法,这方法传了一个参数$dyType,但在哪被调用传入的呢?我们可以看到这个CMysqlColumnSchema类继承了CDbColumnSchema,在CDbColumnSchema类中的init()方法中就调用了extractType(),但init方法中传入的第一个参数还是我们想要知道的$dbType,于是接着找哪个地方又调用了该方法。

我们看
CDbSchema类,这是一个抽象类,CMysqlSchema继承了它,在CDbConnection中使用了CMysqlSchema作为mysql驱动类。
当我们要获得表的schema时,必定要调用到该类中的getTable()方法,而该方法又调用了同类或子类中的loadTable(),意思就是获得表信息,传入的参数就是表名,接着调用了同类中的findColumns()方法,查找出当前表中的字段,方法中使用了SHOW FULL COLUMNS FROM TABLE,然后获得的了一个数组,数组中包含着各个字段名,再使用createColumn()方法将每个字段实例化一个CMysqlColumnSchema实例,那么就又回到上面将的这个类了,此时这里执行该类中的init()方法了(上面标粗的地方),并且传入了数据库获得到的字段类型,此时$dbType的值是int(10) unsigned,也正是我们数据库中设定的类型。

init()方法自然就执行了同类中的extractType()方法,现在我们就仔细看这个类:

protected function extractType($dbType)
{
    if(strncmp($dbType,'enum',4)===0)
        $this->type='string';
    elseif(strpos($dbType,'float')!==false || strpos($dbType,'double')!==false)
        $this->type='double';
    elseif(strpos($dbType,'bool')!==false)
        $this->type='boolean';
    elseif(strpos($dbType,'int')===0 && strpos($dbType,'unsigned')===false || preg_match('/(bit|tinyint|smallint|mediumint)/',$dbType))
        $this->type='integer';
    else
        $this->type='string';
}

其中$dbType的值是”int(10) unsigned”,这是通过SHOW FULL COLUMNS FROM TABLE得来的,那么对应到PHP中该是哪种类型呢?看代码,我们会发现最终程序走了最后的else,这里导致了$this->typestring。也就因此没有转换我们的传入值。

网上我们可以看到这个问题:https://code.google.com/p/yii/issues/detail?id=1820#c1

数据库中无符号的int型在php中如果用integer型可能会失去精度,因此用了string。

解决办法就是建表的时候,建议将每个int型字段不要设置unsigned,毕竟signed int能存入的值已经够我们用了。

这样一来,使用Yii中findByPk()方法就是自动帮我们把传入参数转为integer型了。