树大招风,当然网站被恶意攻击注入是常有的事,这也是对一个程序员代码质量以及经验水平的考验。
今天公司一个项目不幸被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->type
为string
。也就因此没有转换我们的传入值。
网上我们可以看到这个问题:https://code.google.com/p/yii/issues/detail?id=1820#c1
数据库中无符号的int型在php中如果用integer型可能会失去精度,因此用了string。
解决办法就是建表的时候,建议将每个int型字段不要设置unsigned,毕竟signed int能存入的值已经够我们用了。
这样一来,使用Yii中findByPk()
方法就是自动帮我们把传入参数转为integer
型了。