You are here

Drupal专业开发指南 第20章 使用db_query()来确保查询语句的安全

g089h515r806 的头像
Submitted by g089h515r806 on 星期四, 2009-08-27 13:11

攻击网站的一个常见方式称为SQL注入。让我们看一个没有考虑安全性的程序员编写的模块。他仅仅想用一种简单的方式,来列出特定类型节点的所有标题:

 
/*
 * Implementation of hook_menu().
 */
function insecure_menu() {
$items['insecure'] = array(
'title' => 'Insecure Module',
'description' => 'Example of how not to do things.',
'page callback' => 'insecure_code',
'access arguments' => array('access content'),
);
return $items;
}
 
/*
 * Menu callback, called when user goes to http://example.com/?q=insecure
 */
function insecure_code($type = 'story') {
// SQL statement where variable is embedded directly into the statement.
$sql = "SELECT title FROM {node} WHERE type = '$type'"; // Never do this!
$result = db_query($sql);
$titles = array();
while ($data = db_fetch_object($result)) {
$titles[] = $data->title;
}
// For debugging, output the SQL statement to the screen.
$output = $sql;
 
$output .= theme('item_list', $titles);
return $output;
}
 
    访问http://example.com/?q=insecure,一切正常。我们得到了SQL,接着是故事(story)列表,如图20-2所示。
 
20-2。 简单的一列故事节点标题
 
注意,看看这个程序员是如何在这里,聪明的将insecure_code()函数中的$type参数默认设为了'story'。这个程序员利用了Drupal的菜单系统中的一个知识点,路径中的额外变量将自动作为参数传递给回调函数,所以路径http://example.com/?q=insecure/page将为我们得到类型为'page'的节点的所有标题,如图20-3所示。
 
20-3 简单的一列页面节点标题
 
    然而,该程序员犯了一个潜在的致命错误。将变量$type直接放到SQL中,并且依赖于PHP的变量扩展,这样整个站点就完全暴露了。让我们访问http://example.com/?'%20OR%20type%20=%20'story(参看图20-4)。
 
20-4 db_query()中不使用占位符引起的SQL注入
 
哎哟!我们可以在URL中输入SQL并执行它!怎么成这样了?还记不记得,在前面我提到%20是空格的已编码版本?我们简单的输入了以下文本的编码版本:
 
page' OR type = 'story
 
还记不记得我们将不安全的SQL赋值给了$sql变量? 看,当我们输入的编码文本被解码并成为语句的一部分时,发生了什么。下面是以前的代码:
 
SELECT title FROM {node} WHERE type = '$type'
 
替换$type,现在将其设置为了page' OR type = 'story,现在就成了
 
SELECT title from {node} WHERE type = 'page' OR type = 'story'
 
一旦你让用户能够修改发送给数据库的SQL,那么你的站点很容易受到攻击了。下面是改进后的版本:
 
function insecure_code($type = 'story') {
// SQL now protected by using a quoted placeholder.
$sql = "SELECT title FROM {node} WHERE type = '%s'";
$result = db_query($sql, $type);
$titles = array();
while ($data = db_fetch_object($result)) {
$titles[] = $data->title;
}
// For debugging, output the SQL statement to the screen.
$output = $sql;
 
$output .= theme('item_list', $titles);
return $output;
}
 
现在当我们通过访问http://example.com/?q=insecure/page'%20OR%20type%20=%20'story试图操纵URL时,db_query()将通过对单引号进行转义从而清理输入的数值。查询语句将变成下面的样子:
SELECT title FROM node WHERE type = 'page\' OR type = \'story'
 
由于我们没有名为"page\' OR type = \'story"的节点类型,这个查询很明显会失败。然而,我们仍然可以对其进行改进,因为在这种情况下URL应该仅包含一个有限集中的成员;也就是,我们站点上的节点类型。我们知道都有哪些类型,所以我们需要确认用户提供的值是我们已知中的一个。例如,如果我们仅启用了page和story两种节点类型,只有当URL中提供了这些类型时,我们才继续处理。让我们添加一些代码,用来检查这一点:
 
function insecure_code($type = 'story') {
// Check to make sure $type is in our list of known content types.
$types = node_get_types();
if (!isset($types[$type])) {
watchdog('security', 'Possible SQL injection attempt!', array(),
WATCHDOG_ALERT);
return t('Unable to process request.');
}
 
// SQL now protected by using a placeholder.
$sql = "SELECT title FROM {node} WHERE type = '%s'";
$result = db_query($sql, $type);
$titles = array();
while ($data = db_fetch_object($result)) {
$titles[] = $data->title;
}
// For debugging, output the SQL statement to the screen.
$output = $sql;
 
$output .= theme('item_list', $titles);
return $output;
}
 
    这里我们添加了一个检查,用来确保$type是我们的已有节点类型中的一个,如果没有通过检查,那么我们就为系统管理员记录一个便捷的警告日志。然而,还有更多问题。这个SQL没有对发布的和未发布的节点进行区分,所以未发布的节点的标题也会显示出来。另外,节点标题是用户提交的数据,在输出以前需要对其进行安全清理。但是,现在的代码,仅仅将标题从数据库中取出并将其显示了出来。让我们修正这些问题。
 
function insecure_code($type = 'story') {
// Check to make sure $type is in our list of known content types.
$types = node_get_types();
if (!isset($types[$type])) {
watchdog('security', 'Possible SQL injection attempt!', array(),
WATCHDOG_ALERT);
return t('Unable to process request.');
}
 
// SQL now protected by using a placeholder.
$sql = "SELECT title FROM {node} WHERE type = '%s' AND status = 1";
$result = db_query($sql, $type);
$titles = array();
while ($data = db_fetch_object($result)) {
$titles[] = $data->title;
}
 
// Pass all array members through check_plain().
$titles = array_map('check_plain', $titles);
$output = theme('item_list', $titles);
return $output;
}
 
现在只显示已发布的节点了,而所有的标题在显示以前,都经过了check_plain()安全处理。我们还删除了调试模式。这个模块得到了极大的改进!但是还有一个安全漏洞。你能找到吗?如果不能的话,继续阅读本章吧。

老葛的Drupal培训班 Think in Drupal

Drupal版本: