写出安全的YII WEB应用(三) [ 技术分享 ]
接着上篇继续翻译,上篇地址
SQL注入
原理
简单的说,把未经过滤和验证的数据直接拼装SQL语句,会存在SQL注入漏洞。
<?php
// 警告,以下是不安全的写法
Yii::app()->db
->createCommand("DELETE FROM mytable WHERE id = " . $_GET['id'])
->execute();
$comments = Comment::model->findAll("user_id = " . $_GET['id']);
上面示例中的第一个sql语句,如果GET参数是”4 or 1=1”,这会导致表中的所有数据被删除;
第二个sql语句中,如果GET参数是2 UNION SELECT ,会导致数据库的任意数据都被查询出来。
YII如何防范
使用YII提供的函数操作
如下例
<?php
$id = intval($_GET['id']);
MyModel::model()->findByPk($id)->delete();
// 使用类型转换
$comments = Comment::model->findAllByAttributes(array('user_id' => (int)$_GET['id']);
使用YII函数要比纯SQL语句安全些;但,对于YII函数来说,使用数组比字符串更安全;如下例:
<?php
//存在sql注入
$comments = Comment::model->findAll("post_id = $postId AND author_id IN (" . join(',', $ids) . ")");
// 安全
$comments = Comment::model->findAllByAttributes(array("post_id" => $postId, "author_id" => $ids));
SQL语句预编译
当必须要使用原生的SQL的时候,比如一个SQL语句有两个参数,如下所示:
SELECT CONCAT(prefix, title) AS title, author_id, post_id, submit_date
FROM t_comment
WHERE (date > '{$date}' OR date IS NULL) AND title LIKE '%{$text}%'
遇到这种情况,有以下两种相对安全的写法:
- 给每个参数加引号(不推荐)
- 使用预编译SQL(推荐)
当使用第一种方式的时候,可以使用YII的CDbConnection::quoteValue();
比如"date > '{$date}'"
,可以写成"date > " . Yii::app()->db->quoteValue($date)
。
数据库服务器先编译完传入的SQL语句,再将接收到的参数插入到SQL语句的占位符。但,当数据库服务器不支持预编译时,PHP就会模拟这个过程,这也可能有SQL注入的隐患(预编译的详细原理可以参考这篇博文)。
在YII中SQL预编译的过程可以如下所示的代码:
<?php
// 占位符没有引号
$sql = "SELECT CONCAT(prefix, title) AS title, author_id, post_id, date "
. "FROM t_comment "
. "WHERE (date > :date OR date IS NULL) AND title LIKE :text"
// 第一种写法
$command = Yii::app()->db->createCommand($sql);
$command->bindParam(":date", $date, PDO::PARAM_STR);
$command->bindParam(":text", "%{$text}%", PDO::PARAM_STR);
$results = $command->execute();
// 第二种写法
$command = Yii::app()->db->createCommand($sql);
$results = $command->execute(array(':date' => $date, ':text' => "%{$text}%"));
当使用ActiveRecordr的时候用SQL预编译,语法会更加简练,如下所示:
<?php
$comments = Comment::model->findAllBySql($sql, array(':date' => $date, ':text' => "%{$text}%"));
对SQL语句中LIKE的一些补充
在上面的示例中,即使不存在SQL注入隐患,此SQL语句中的like的使用也值得商榷。’%like%’不使用索引的,而’like%’是可以使用索引的;所以,如果将’like%’转换成’%like%’,当like的字段值很大的时候,会严重影响效率。
建议当需要使用到’%like%’的时候,尽量使用其它比较符(<=,>,……)替换。可以使用YII的CDbCriteria::compare()
和CDbCriteria::addSearchCondition()
函数,而简化操作。
对预编译SQL中参数占位符补充
从YII1.1.8起,占位符不再用”?”标识,而是使用”:”标识;
对预编译SQL的效率的补充
预编译稍长的SQL要比不编译要稍慢些,这对系统的整体性能影响非常小。但如果同一个SQL运行多次,预编译的效率优势就体现出来了。然而,如果使用的PHP模拟预编译,则跟不编译SQL没有区别。
如果预编译不满足应用的实际需求
虽然预编译能防止SQL注入,但有些时候因为SQL语句的各个部分都是变量,所以不能使用预编译。如下所示:
SELECT *
FROM {$mytable}
WHERE {$myfield} LIKE '{$value}%' AND post_date < {$date}
ORDER BY {$myfield}
LIMIT {$mylimit}
遇到这类情况,一般使用白名单过滤SQL语句的每个部分。YII提供如下类似的过滤方法:
<?php
if (!Comment::model()->hasAttribute($myfield)) {
die("Error");
}
更加常用的是使用YII的” Query Builde”,但不能跟CDbCriteria结合使用。 多数时候,我们可能是通过Model来查询,可以使用find*()类的方法与CDbCriteria一起使用。如下:
<?php
// YII会检测字段的合法性
$criteria = new CDbCriteria(
array(
'order' => $myfield,
'limit' => $mylimit,
)
);
$criteria->compare($myfield, $value, true); // LIKE % :$value会被转义
$criteria->compare('post_date', '<:date');
$criteria->params = array(':value' => $value, ':date' => $date);
$comments = Comment::model()->findAll($criteria)
YII的GII模块使用CGridView提供数据。CDataProvider使用CDbCriteria为CGridView提供数据,所以当使用CGridView的时候,YII会自动过滤与验证用户输入的查询条件。
一个完整的示例如下:
<?php
// 当不是原生的SQL语句的时候,YII会自动验证字段的合法性
$criteria = new CDbCriteria();
$criteria->order = $myfield;
$criteria->limit = $mylimit;
$criteria->addSearchCondition($myfield, $value, true); // true ==> LIKE '%...%'
$criteria->addCondition("post_date < :date");
$comments = Comment::model()->findAll($criteria, array(':value' => $value, ':date' => $date));
SQL注入的总结
当使用Model进行查询时,有如下五种方式:
1. CActiveRecord::findByPk() 或者 CActiveRecord::findAllByPk().(推荐)
2. CActiveRecord::findByAttributes() 或者 CActiveRecord::findByAttributes()
3. X::model()->find($criteria, array(':param1' => $value1)) 或者 ->findAll(...)
4. X::model()->find($sql, array(':param1' => $value1)) 或者->findAll(...)
5. X::model()->findBySql($sql, array(':param1' => $value1)) 或者 ->findAll(...)
当不是基于Model查询时,要使用预编译,如下所示:
<?php
$r = Yii::app()->db
->createCommand($sql)
->queryAll(array(':param1' => $value1));
使用这种方式时,切记要对用户输入的进行过滤与验证。
共 1 条回复
yedong0839 成都
最后登录:2019-07-16
在线时长:27小时57分
- 粉丝14
- 金钱1980
- 威望0
- 积分2250