Drupal专业开发指南 第20章 编写安全的代码

几乎每天我们都可以看到关于安全漏洞的头条消息,这种或者那种软件出现了漏洞。对于每个严谨的开发者来讲,将恶意用户拒之门外就是头等大事。

一个恶意用户可以使用多种方式来攻击你的Drupal站点。这些攻击方式包括,向你的系统中注入代码并让它执行,操纵你数据库中的数据,查看用户无权访问的资料,通过你的Drupal站点发送垃圾邮件。在本章,我们将学习如何编写安全的代码来阻止这些攻击。
幸运的是,Drupal提供了一些工具,使用它们你就可以很容易的消除这些常见的安全漏洞。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 处理用户输入

 

当用户与Drupal交互时,一般都是通过一系列的表单来完成的,比如节点提交表单、评论提交表单。用户也可能使用blogapi module来发布一个基于Drupal的日志。Drupal的用户输入方式可以总结为“存储原始的;过滤输出的”(store the original; filter on output)。这样,数据库中总会保存一份与用户输入内容完全一致的备份。当用户输入的内容准备用来生成web页面输出时,就应该进行过滤了,这样潜在的恶意代码就被中和了。
当用户输入的内容没有经过安全处理就在你的程序中执行时,就可能引起安全漏洞。当你编写你的程序时,如果你没有全面的考虑各种情况的话,就会留下安全隐患。你可能期望用户输入标准的字符,而事实上他们可以输入非标准的字符串,比如控制字符。你可能看到其中包含字符%20的URL;例如,http://example.com/my%20document.html。这是一个为了与URL规范(参看http://www.w3.org/Addressing/URL/url-spec.html)兼容,对空格字符进行编码后的结果。当有人保存了一个名为my document.html的文件,并且可从web服务器上请求它,那么就会对空格编码。%意味着编码了的字符,而20则因为这它是ASCII字符 32(这里的20是32的16位进制表示)。恶意用户可通过各种诡计,来使用编码字符给你的网站带来麻烦,你将会在本章的后面看到这一点。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 考虑数据类型

当在一个像Drupal这样的系统中处理文本时,由于用户输入将会作为站点的一部分展示出来,所以如果将用户输入看作一个带有类型的变量,那么就能帮我们很好的理解这个系统。如果你使用过强类型语言写过程序,比如JAVA,那么你将熟悉强类型变量。例如,在Java中,一个整数就是一个整数。在PHP(弱类型语言)中,使用PHP的自动类型转换,根据上下文,你既可以把整数看作字符串,也可以看作整数。但是优秀的PHP程序员都会仔细的考虑类型,并恰到好处的利用自动类型转换。同样,尽管是通过用户输入得到的,节点提交表单中的“Body”(主体)字段,也可以作为一个文本进行处理,如果我们把它看作具有特定类型的文本,那么就会更好的理解的它的本质。用户输入的是纯文本么?用户输入的文本中是否带有HTML标签,如果带有的话是否将它们也一同显示出来?如果带有HTML标签的话,这些标签中是否允许带有恶意的标签,比如JavaScript,它可以将你的页面替换成一个手机铃声的广告?在展示给用户的页面中,采用HTML格式;用户输入是各种文本格式类型的变体,在展示它们以前,必须安全的将其转化为HTML。如果我们使用这种方式来考虑用户输入的话,这能够帮助我们理解Drupal的文本转换功能的工作原理。文本输入的常见类型,还有将文本转化为另一种格式的函数,如表20-1所示。

 
20-1 将一种文本类型安全的转化为另一种类型
源格式      目标格式            Drupal 函数              功能
Plain text         HTML               check_plain()           将特定字符编码为HTML实体
HTML text         HTML               filter_xss()       使用一组标签,检查和清理HTML
Rich text          HTML               check_markup()     使用过滤器过滤
Plain text         URL                drupal_urlencode() 将特定字符编码为%0x
URL                HTML               check_url()        清除有害的协议,比如javascript,
Plain text         MIME               mime_header_encode() 编码非ASCII字符, UTF-8编码字符
 

老葛的Drupal培训班  Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 Plain text(纯文本)

Plain text(纯文本)

Plain text 是仅仅包含纯文本的文本。例如,如果你让一个用户在一个表单中键入他/她喜欢的颜色,你期望用户输入“green”(绿色)或者 “purple”(紫色),而不包含任何标识字体。如果在另一个网页中,包含这个输入框,但是却没有不进行任何检查来确保它真的是仅仅包含纯文本,那么就会留下安全漏洞。例如用户没有输入一个颜色,而输入了一下内容:
 
 
因此,我们可以使用check_plain()来确保通过将HTML标签转义为HTML实体来消除潜在的危害。从check_plain()返回的文本不包含任何HTML标签,因为将它们转换为相应的实体了。如果一个用户输入了前面的恶意JavaScript代码,那么check_plain()函数就会将其转化为以下文本,现在它就没有危害了:
 
<img src="javascript:window.location ='<a
 
HTML text(HTML文本)
HTML text 可以包含HTML标识字体。然而,你永远不要盲目的相信用户,仅仅输入了安全的HTML;一般情况下,你想将用户能够使用标签限制在一个特定的可用的HTML标签子集中。例如,<script>一般都会被你禁用,因为它允许用户在你的站点上运行他们选择的脚本。同样,你也不想让用户使用<form>标签,以在你的站点上建立表单。
 
Rich text(富文本)
Rich text是比纯文本包含更多信息的文本,它不一定必须是HTML。它可以包含wiki标识字体,或者论坛代码(BBCode),或者其它的标识语言。在展示以前,必须使用一个过滤器来将这些文本转化为HTML。
 
注意 更过关于过滤器的信息,参看第11章。
 
URL
URL是由用户输入的,或者其它不可信的地方获取的URL。你可能希望用户输入http://example.com,但是用户却输入了javascript:runevilJS()。在将URL展现在HTML页面以前,你必须使用check_url()来对其进行检查,以确保它的格式良好并且不包含任何攻击。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 使用check_plain()和t()来清理输出

当你对你用到的文本不信任,并且你不想在文本中有任何markup(标识字体)时,那么就可以使用check_plain()。

下面是使用用户输入的原始方式,假定用户刚刚在一个文本输入框中输入了一个喜欢的颜色。
下面的代码不安全:
 
drupal_set_message("Your favorite color is $color!"); // No input checking!
 
下面的代码安全,但不是最佳实践:
 
drupal_set_message('Your favorite color is ' . check_plain($color));
 
这段代码不好,这是因为它没有把这个文本字符串(这里指的是check_plain()函数输出的结果)放到t()中,对于文本字符串总是需要调用该函数的。如果你像上面这样编写代码,那你就等着挨骂吧,翻译者将不能够翻译你的语句,因为它没有使用t()函数。
你不能将变量直接放到双引号中,并将它们传递给t()。下面的代码仍然不安全,因为它没有使用占位符:
 
drupal_set_message(t("Your favorite color is $color!")); // No input checking!
 
t()函数提供了一种内置的方式用来确保你的字符串的安全性,使用一个带有单字符前缀的占位符,如下所示。
下面的代码很安全并且格式良好:
 
drupal_set_message(t('Your favorite color is @color', array('@color' => $color));
 
    注意数组中的键(@color)与字符串中的占位符完全相同。消息的结果如同下面的这样:
 
Your favorite color is brown.
 
前缀@告诉t(),要对替换占位符的值调用check_plain()。
 
注意 当运行一个Drupal 的t()函数时,将对占位符的值调用check_plain(),对于其它的字符串则不调用check_plain()。所以你需要信任你的翻译者。
 
在这里,我们可能想通过改变颜色值的样式,来强调用户所选择的颜色。使用%前缀可以达成所愿,它意味着“对于该值执行theme('placeholder', $value)”。它间接的将值传递给了check_plain(),如图20-1所示。前缀%是最常用的前缀。
 
       下面的代码是安全的并且格式良好:
 
drupal_set_message(t('Your favorite color is %color', array('%color' => $color));
 
一个消息产生的结果如下所示。使用theme_placeholder()对该值进行了主题化,它简单的使用了<em></em>标签对值进行了包装。
 
Your favorite color is brown.
 
    如果你的文本在前面已被清理过了,你可以使用前缀!来禁用t()中的检查。例如,l()函数构建了一个链接,在构建链接时,它通过check_plain()处理了链接文本。所以在下面的例子中,可以安全的使用前缀!:
 
// l() function runs text through check_plain() and returns sanitized text
// so no need for us to do check_plain($link) or to have t() do it for us.
$link = l($user_supplied_text, $path);
drupal_set_message(t('Go to the website !website', array('!website' => $link));
 
注意 除非你通过在选项参数中将html设置为TRUE,以对l()指示链接文本已经是HTML格式了,否则l()函数仍然将链接文本通过check_plain()进行处理。参看http://api.drupal.org/api/function/l/6
 
t()函数中,用于字符串替换的@, %, 和 !占位符的作用,如图20-1所示。在该图中我们是用了一个简单的例子进行说明,记住你可以在字符串中使用多个占位符并将它们添加到数组中,例如:
 
drupal_set_message(t('Your favorite color is %color and you like %food',
array('%color' => $color, '%food' => $food)));
 
使用前缀!时要小心一点,因为该字符串没有经过check_plain()处理。
 
20-1 字符串替换时,不同占位符前缀的作用
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 使用filter_xss()阻止跨站点脚本攻击

跨站点脚本(XSS)是攻击网站的一种常用方式,攻击者可以向一个网页插入他/她自己的代码,然后使用这些代码进行各种破坏活动。

 
     注意:XSS攻击的例子,参看http://ha.ckers.org/xss.html
 
假定你允许用户向你的网站输入HTML,期望他们这么输入
 
<em>Hi!</em> My name is Sally, and I...
 
但是他们输入了
 
 
    哎哟!我们又学了一课:永远不要信任用户输入。下面是函数filter_xss()的签名:
filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'code',
'ul', 'ol', 'li', 'dl', 'dt', 'dd'))
 
    函数filter_xss()对传递给它的文本字符串进行以下操作:
 
1.它通过检查确保被过滤的文本是有效的UTF-8,从而阻止IE6下面的bug。
 
2. 它删掉诡异的字符比如NULL和Netscape4 JavaScript实体。
 
3. 它确保HTML实体比如&amp; 形式正确。
 
4.它确保HTML标签和标签属性的形式正确。在本阶段,没有出现在允许列表中——也就是,filter_xss()中的第2个参数——的标签,将被删除。属性style也被删除,这是由于通过覆盖CSS,它能够影响页面的外观,或者通过将页面的背景颜色设为一个垃圾链接的颜色来隐藏内容。以on开头的任何属性都会被删除(比如,onclick 或者 onfocus)如果你熟悉正则表达式,并且能够记住HTML实体的字符编码的话,你可以使用一个调试器来一步一步的学习一下filter_xss()(位于modules/filter/filter.module)以及它的相关函数。
 
5. 它确保所有的HTML标签,都不包含不允许的协议。允许的协议有http,https, ftp, news, nntp, telnet, mailto, irc, ssh, sftp, 和 webcal。你可以通过设置filter_allowed_protocols变量来修改这一列表。通过将下面的代码添加到你的settings.php文件中(参看settings.php文件中的关于变量覆写的注释),可以将允许的协议限制为http 和 https:
$conf = array(
'filter_allowed_protocols' => array('http', 'https')
);
 
下面的filter_xss()例子来自于modules/aggregator/aggregator.pages.inc,聚合器模块处理存在潜在安全隐患的RSS 或者 Atom种子。在下面,该模块为显示种子项模板文件准备使用的变量:
 
/**
 * Process variables for aggregator-item.tpl.php.
 *
 * @see aggregator-item.tpl.php
 */
function template_preprocess_aggregator_item(&$variables) {
$item = $variables['item'];
 
$variables['feed_url'] = check_url($item->link);
$variables['feed_title'] = check_plain($item->title);
$variables['content'] = aggregator_filter_xss($item->description);
...
}
 
注意我们调用了aggregator_filter_xss(),它对filter_xss()进行了封装并提供了一组可接受的HTML标签。我们将这个函数稍微做了简化,如下所示:
 
/**
 * Safely render HTML content, as allowed.
 */
function aggregator_filter_xss($value) {
$tags = variable_get("aggregator_allowed_html_tags",
'<a> <b> <br> <dd> <dl> <dt> <em> <i> <li> <ol> <p> <strong> <u> <ul>');
// Turn tag list into an array so we can pass it as a parameter.
$allowed_tags = preg_split('/\s+|<|>/', $tags, -1, PREG_SPLIT_NO_EMPTY));
return filter_xss($value, $allowed_tags);
}
 
     注意 作为安全性的一个练习,你可以拿出你自己的一些定制模块,来追踪输入到系统中了的用户输入,一定要确保在进行逻辑处理之前,先对用户输入进行清理,以保证安全性。
老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 使用filter_xss_admin()

有时你想让你的模块为后台管理页面生成HTML。由于我们对后台管理页面进行了访问控制,所以我们可以假定这些可以访问后台管理页面的用户比普通用户更可信。你可以为后台管理页面建立一个特定的过滤器并使用过滤器系统,但是这有点麻烦。因此,Drupal提供了函数filter_xss_admin()。它使用一组更加自由的可用标签列表,简单的对filter_xss()做了封装,除了<script> 和 <style>标签以外,它包含了所有的其它标签。使用它的一个例子是在主题中展示站点的宗旨(mission):

 
if (drupal_is_front_page()) {
$mission = filter_xss_admin(theme_get_setting('mission'));
}
 
只有在管理设置页面Administer >> Site configuration >>“Site information”才可以设置站点的宗旨,而只有超级用户和具有“administer site configuration”(管理站点配置)权限的人才可以访问这一页面,所以在这里使用filter_xss_admin()就比较合适。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 安全的处理URLs

模块常常处理用户提交的URLs并显示它们。我们需要一些机制来确保用户提供的值确实是一个合法的URL。Drupal提供了函数check_url(),它实际上是仅仅对filter_xss_bad_protocol()做了封装。它通过检查来确保URL中的协议是该Drupal站点所允许的协议(参看“使用filter_xss()阻止跨站点脚本攻击”部分的第5步),并使用check_plain()来处理URL。

如果你想判断一个URL是否合法,你可以使用valid_url()。它将检查http, https, 和 ftp URL的语法,并检查非法字符;如果URL通过了测试,那么它返回TRUE。这是一个便捷的方式,用来确保提交的URLs不包含javascript协议。
 
警告 仅仅通过语法检查的URL并不一定安全!
 
如果你使用URL——例如,在一个查询字符串中——来传递一些信息的话,你可以使用drupal_urlencode()来传递转义了的字符。调用drupal_urlencode()对斜杠,反斜杠,符号&进行编码,以兼容Drupal的简洁URL,接着调用PHP的rawurlencode()函数。函数drupal_urlencode()并不比直接调用rawurlencode()更安全,但是它可以方便的对字符串进行编码,以适应Apache的mod_rewrite模块。
 
提示 drupal_urlencode()是对PHP函数封装的一个例子:你可以直接调用PHP的urlencode(),但这样你将失去由Drupal为你负责一个函数的好处。类似的字符串封装函数可参看unicode.inc;例如,使用drupal_strlen()来代替PHP函数strlen()。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

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

攻击网站的一个常见方式称为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版本:

Drupal专业开发指南 第20章 使用db_rewrite_sql()来保持私有数据的私有性

老葛的Drupal培训班 Think in Drupal

在前面的列出节点这一例子,对于第3方模块来说是个常见的任务(现在的用的少一些了,这是由于使用views模块,可以很方便的通过web定义节点列表)。你可能会问:如果站点启用了节点访问控制模块,在前面的例子中哪段代码是用来保证我们的用户仅看到允许他们看到的节点集?这个问题问得很好,确实没有相应的代码。前面的代码将显示给定节点类型的所有节点,即便是节点访问模块限制访问的节点,也被显示了出来。这段代码非常傲慢,它没有考虑其它模块的感受!让我们修改一下代码。
    修改前:
 
$sql = "SELECT title FROM {node} WHERE type = '%s' AND status = 1";
$result = db_query($sql, $type);
 
    修改后:
 
$sql = "SELECT n.nid, title FROM {node} n WHERE type = '%s' AND status = 1";
$result = db_query(db_rewrite_sql($sql), $type); // Respect node access rules.
 
我们使用db_rewrite_sql()来对传递给db_query()的SQL参数进行了包装,函数db_rewrite_sql()允许其它模块修改SQL。传递给db_rewrite_sql()的查询语句,需要生命它们的主键(n.nid)和表的别名(n),所以我们在这里加了进来。核心模块中的一个重要例子就是节点模块,它改写针对node表的查询语句的。它首先检查node_access中是否存在可能限制用户访问节点的记录,然后向SQL中插入查询语句片段,用来检查这些权限。在我们的这种情况下,节点模块将修改SQL,它向WHERE语句中插入一个AND查询片段,从而过滤用户无权访问的节点。如何实现这一点,以及更多关于db_rewrite_sql()的信息,请参看第5章。
 

Drupal版本:

Drupal专业开发指南 第20章 动态查询语句

如果在你的SQL中,有多个只有在运行时才能确定的变量,这并不妨碍让你使用占位符。你将需要使用占位符比如'%s' 或者 %d,通过编程来创建你的SQL,接着你需要传递一组值来填充这些占位符。如果你自己直接调用db_escape_string(),那么你就做错了。下面的例子展示了占位符的使用,这里假定我们想取出,匹配特定节点类型的已发布节点的ID和标题:

 
// $node_types is an array containing one or more node type names
// such as page, story, blog, etc.
$node_types = array('page', 'story', 'blog');
 
// Generate an appropriate number of placeholders of the appropriate type.
$placeholders = db_placeholders($node_types, 'text');
 
// $placeholders is now a string that looks like '%s', '%s', '%s'
$sql = "SELECT n.nid, n.title from {node} n WHERE n.type IN ($placeholders)
AND status = 1";
 
// Let db_query() fill in the placeholders with values.
$result = db_query(db_rewrite_sql($sql), $node_types);
 
    运行完db_rewrite_sql()以后,db_query()的调用看起来的样子如下所示:
 
db_query("SELECT DISTINCT(n.nid), n.title from {node} n WHERE n.type IN
('%s','%s','%s') AND status = 1", array('page', 'story', 'blog'));
 
现在当db_query()执行时,将对节点类型的名称进行清理。如果你想知道具体的缘由,可参看includes/database.inc中的db_query_callback()。
下面是另一个例子。有时你处在这样的一种情况,你想在一个查询的WHERE语句中添加一些AND限制条件来限制查询语句。此时,你也需要小心点使用占位符。在下面的例子中我们假定$uid 和 $type的值都是有效的(例如,3和page)。
 
$sql = "SELECT n.nid, n.title FROM {node} n WHERE status = 1";
$where = array();
$where_values = array();
 
$where[] = "AND n.uid = %d";
$where_values[] = $uid;
 
$where[] = "AND n.type = '%s'";
$where_values[] = $type;
 
$sql = $sql . ' ' . implode(' ', $where) ;
// $sql is now SELECT n.nid, n.title
// FROM {node} n
// WHERE status = 1 AND n.uid = %d AND n.type = '%s'
 
// The values will now be securely inserted into the placeholders.
$result = db_query(db_rewrite_sql($sql), $where_values));
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 权限和页面回调

当你编写自己的模块时,需要注意的另一个方面是“access arguments”键,你会在菜单钩子定义中的每个菜单项中用到它。在前面我们用来说明不安全代码的例子中,我们使用了下面的access参数:

 
/*
 * 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;
}
 
一个非常重要的问题是,允许谁访问这个回调函数。“access content”(“访问内容”)权限是个很宽泛的权限。你可能想使用hook_perm()来定义你自己的权限,并使用它们来保护你的菜单回调函数。权限就是一些唯一的字符串,用来描述权限。(更多详细信息可参看第4章的“访问控制”部分)。
由于你实现的菜单钩子实际上就是个门卫,它允许或者拒绝用户对菜单钩子后面代码的访问(通过回调),对于你在这里使用哪些权限,你需要认真的考虑一下。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 跨站点请求伪造(CSRF)

老葛的Drupal培训班 Think in Drupal

假定你登录到了drupal.org,在里面浏览论坛中的内容。接着,由于其它原因,你转而浏览另一个网站。有人在该站点上不怀好意的放了一个图片标签:
 
 
当你的浏览器加载该图片时,它将会对drupal.org请求该路径。由于你当前已经登录到了drupal.org,你的浏览器会在请求中把你的cookie也带上。现在就需要考虑一个问题了:当drupal.org收到这个请求时,它会把这个请求当作一个以登录的用户进行处理吗?你猜测它会!恶意用户的图片标签,以你的用户名点击了一个drupal.org上的链接。
首先,对于此类攻击,你要做的是永远不要使用GET请求来修改服务器上的东西;这样生成的任意请求都将无害。Drupal表单API遵循HTTP/1.1公约,GET方法除了数据检索以外不应采取任何行动。对于需要对服务器进行改动的动作,Drupal全部使用POST(参看http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1)。
其次,表单API使用令牌和标识ID,来确保从POST请求中提交的表单数据来自于Drupal生成的表单(更多详情,参看第10章)。当你编写模块时,对于你的表单,一定要为其使用表单API,这样你就能够自动的获得这一保护。在你的模块中,对表单输入结果进行的任何操作,都应该放到该表单的提交函数中。这样你就可以确保表单API对你进行了保护。
最后,如果需要的话,你还可以在URL中加个令牌(由drupal_get_token()生成)并使用drupal_valid_token()对令牌进行验证,来保护GET请求。

Drupal版本:

Drupal专业开发指南 第20章 文件安全

在处理文件和文件路径时,Drupal面对的危险和其它web应用是一样的。

 
文件权限
文件权限应该这样设置,那就是用户不能操作(添加,重命名,或者删除)文件。Web服务器对于Drupal文件和目录,应该仅具有只读的权限。不过有个例外,那就是文件系统路径。很明显,web服务器必须对该目录具有访问权限,这样才能写入上传文件。
 
受保护文件
Drupal自带的.htaccess文件包含了以下代码:
 
# Protect files and directories from prying eyes.
<FilesMatch "\.(engine|inc|info|install|module|profile|po
|sh|.*sql|theme|tpl(\.php)?|xtmpl)$|^(code-style\.pl
|Entries.*|Repository|Root|Tag|Template)$">
Order allow,deny
</FilesMatch>
 
Order指令设为了allow,deny,但是却没有包含Allow或者Deny指令。这意味着默认的行为是拒绝。换句话说,对表20-2所示文件的请求都会被拒绝访问。
 
20-2.根据FilesMatch指令的正则表达式所拒绝的文件
文件                 匹配描述
后缀 .engine         模板引擎
后缀.inc             库文件
后缀.info            模块和主题的.info文件
后缀.install         模块的.install文件
后缀.module          模块文件
后缀.profile         安装器
后缀.po              PO文件(翻译)
后缀.sh              Shell脚本
后缀.*sql            SQL脚本
后缀.theme           PHP主题
后缀.tpl.php         PHPTemplate模板文件
后缀.tpl.php4         PHPTemplate模板文件
后缀.tpl.php5         PHPTemplate模板文件
后缀.xtmpl            XTemplate文件
名为 code-style.pl    语法检查脚本
前缀为Entries.        CVS文件
名为Repository        CVS文件
名为Root              CVS文件
名为Tag               CVS文件
名为Template          CVS文件
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 文件上传

如果一个启用的模块允许文件上传,那么文件就应该放到一个特定的目录下,并通过代码来强制访问。

如果启用了文件上传,并在Administer >>Site configuration >> File system中选择了私有下载方法,那么该页面的文件系统路径必须位于web根目录以外。换句话说,如果强迫在特定应用的文件用户权限与web根目录保持一致,会恰得其反的。
文件上传的最大危险就是,如果有人能够上传一个可执行的文件,那么该文件就可以用来获取你服务器上的更多权限。Drupal从两方面对这一点进行了保护。首先,会向文件系统路径声明的目录写入.htaccess文件:
 
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
Options None
Options +FollowSymLinks
 
指令SetHandler告诉Apache,这个目录下面的任何可执行文件,应有处理器Drupal_Security_Do_Not_Remove_See_SA_2006_006(实际并不存在)进行处理。这样,该处理器九覆写了由Apache定义的任意处理器,比如
 
AddHandler application/x-httpd-php .php
 
Drupal的上传模块还为多个扩展名的文件实现了重命名。这样,evilfile.php.txt经过上传就变成了evilfile.php_.txt。更多详细可参看http://drupal.org/node/65409http://drupal.org/node/66763
 
注意 前面的方案是针对Apache的。如果你的Drupal使用的是不同的web服务器,那么你应该清楚,如何解决用户可能上传可执行文件这一安全问题。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 文件名和路径

不要相信用户提供的文件名或者文件路径!当你编写一个模块时,你的代码期望能够收到somefile.txt,实际上它可能得到了其它文件,比如

 
../somefile.txt // File in a parent directory.
../settings.php // Targeted file.
somefile.txt; cp ../settings.php ../settings.txt // Hoping this runs in shell.
 
前面的两个例子,里面包含了两个.号,这意味着想操作当前目录的上一级目录。后面的一个,就是希望程序员执行一个shell命令,它包含了一个分号,这样该shell命令运行以后,将会运行一个额外的命令,以将settings.php文件设为可读,从而获取数据库用户名和密码。在前面的所有的例子中,都希望web服务器对文件系统路径以外的目录具有写权限。
无论你什么时候使用文件路径,都须调用file_check_location(),就像这样:
 
if (!file_check_location($path, 'mydirectory') {
// Abort! File path is not what was expected!
}
 
file_check_location()函数将找出文件的实际位置,并将其与你期望的目录进行对比。如果文件路径为OK,那么就返回文件的实际路径;否则,返回0。
一般来讲,你可能不会开始就想编写一个了不起的文件管理模块供大家使用。实际上你会,先学习现有的相关模块,熟悉了以后,再编写自己的文件模块。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 对邮件头部进行编码

老葛的Drupal培训班 Think in Drupal

当你编写的代码需要使用用户输入,并将其构建到e-mail消息中时,需要考虑一下两点:
 
1. E-mail头部用换行符(line feed)来分隔(只有在换行符后面没有空格或者tab键时才被作为头部分隔符)
 
2..如果你没有检查用户的输入是否包含换行符的话,那么用户就可以在邮件的主体中注入它们自己的头部。
 
例如,假如你希望用户为他/她的消息输入一个标题,而用户输入了以下内容,它包含转义的换行符 (%0A)和空格(%20)字符:
 
Have a nice day%0ABcc:spamtarget@example.com%0A%0AL0w%20c0st%20mortgage!
 
结果将如下所示:
 
Subject: Have a nice day
L0w c0st mortgage!
...
 
因此,Drupal内置的邮件函数drupal_mail()使用mime_header_encode()来过滤所有的头部。所有的不能打印的字符将根据RFC 2047编码为ASCII可打印的字符,这样就消除了潜在的风险。这涉及到在字符前面添加前缀=?UTF-8?B?,接着输出基于64位编码的字符,再加上?=。
我们鼓励你使用drupal_mail();如果你不使用它的话,那么你需要自己直接调用mime_header_encode()。
 

Drupal版本:

Drupal专业开发指南 第20章 用于生产环境的文件

并不是Drupal自带的所有文件,都是生产站点所必须的.例如,如果在一个生产站点上,有CHANGELOG.txt文件可用的话,那么互联网上的任何人都可以查看你的Drupal版本了(当然,一些高手黑客可用多种方式来探测你的网站是不是使用的Drupal;参看http://www.lullabot.com/articles/is-site-running-drupal)。表20-3列出了Drupal安装后,为了正常工作所需要的文件和目录;其它的都可以从生产站点上删除(不过要保留一份备份哦!)当然,你也可以采用另外一种方式,那就是限制对这些文件的读权限。

 
20-3. Drupal正常工作所需要的文件和目录
文件/目录             目的
.htaccess       在Apache上用于安全,简洁URL和缓存支持
cron.php        允许运行周期性调度任务
includes/       函数库
index.php       请求的主入口
misc/           JavaScript和图片
modules/        核心模块
robots.txt      阻止恶意的网络爬虫骚扰你的网站
sites/          站点特定的模块,主题和文件
themes/         核心主题
xmlrpc.php      XML-RPC终端;只有当你的站点需要接收XML-RPC请求时才有用。
老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 保护cron.php

Drupal中,有些周期性的调度任务是必须执行的,比如清理日志文件,更新统计等等。对于Unix系统,你可以使用cron任务,对于Windows,你可以使用任务调度器,来运行cron.php文件。可以通过命令行或者通过web服务器来运行该文件。在这个文件的执行中,它简单的做了一个完整的Drupal引导指令,并调用includes/common.inc中的drupal_cron_run()函数。这个函数使用信号量来阻止cron的重负运行(一个cron周期运行多次);尽管如此,特别小心的用户可能还想阻止任何用户访问http://example.com/cron.php。你可以通过在Drupal根目录下的.htaccess文件中添加以下代码,来实现这一点:

<Files cron.php>
Order deny,allow
Deny from all
Allow from example.com
Allow from 1.2.3.4
Allow from 127.0.0.1
</Files>
前面的指令,告诉Apache,拒绝任何客户对cron.php的访问,这里面还给出了特殊情况,example.com域名,IP地址为1.2.3.4的计算机,以及本地机器。
有些管理员会简单得将cron.php文件进行重命名。
老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 SSL支持

SSL支持

默认情况下,Drupal通过HTTP使用纯文本来处理用户登录。然而,如果你的web服务器支持HTTPS的话,那么Drupal也可以通过HTTPS来处理登录。此时不需要做任何修改。
 
独立的PHP
偶尔,你可能需要编写一个独立的.php文件来取代将代码放到Drupal的模块中。当你这样做时,你需要仔细的考虑安全性。假如,当你正在测试你的网站时,你快速的编写了一些有点垃圾的代码用来向数据库中插入一些用户,这样你就可以使用多用户来测试站点性能了。假定你将其命名为testing.php,并将其放到了Drupal站点的根目录下,挨着index.php。接着,你把它放到了浏览器的收藏夹中了,当你每次想插入一些新用户时,你就可以选择收藏夹中的该链接了:
 
<?php
/**
 * This script generates users for testing purposes.
 */
// These lines are all that is needed to have full
// access to Drupal's functionality.
include_once 'includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
 
db_query('DELETE FROM {users} WHERE uid > 1'); // Whoa!
for ($i = 2; $i <= 5000; $i++) {
$name = md5($i);
$pass = md5(user_password());
$mail = $name .'@localhost';
$status = 1;
db_query("INSERT INTO {users} (name, pass, mail, status, created, access)
VALUES ('%s', '%s', '%s', %d, %d, %d)", $name, $pass, $mail, $status,
time(), time());
}
print t('Users have been created.');
 
 这对于测试很有用,如果你一不小心把这个脚本放到了你真实的站点上,那么将会发生什么呢?任何发现了这个脚本的人都可以使用一个简单的请求来删除你数据库中的用户数据。这就是为什么安全检查这么重要,即便是对于一次性的脚本来说,也需要包含它,如下所示:
 
<?php
/**
 * This script generates users for testing purposes.
 */
// These lines are all that is needed to have full
// access to Drupal's functionality.
include_once 'includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
 
// Security check; only superuser may execute this.
if ($user->uid != 1) {
print t('Not authorized.');
exit();
}
 
db_query('DELETE FROM {users} WHERE uid > 1'); // Whoa!
for ($i = 2; $i <= 5000; $i++) {
$name = md5($i);
$pass = md5(user_password());
$mail = $name .'@localhost';
$status = 1;
db_query("INSERT INTO {users} (name, pass, mail, status, created, access)
VALUES ('%s', '%s', '%s', %d, %d, %d)", $name, $pass, $mail, $status, time(),
time());
}
print t('Users have been created.');
 
通过这个例子领会到以下两点:
 
1.即便是在快速编写的脚本中也要包含安全检查,你可以从一个包含必要代码的模板做起。
2.记住在部署中的重要一步,就是删除或者禁用测试代码。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 Ajax安全性

 

当你需要使用Ajax比如jQuery时,你一般在开发Ajax的服务器端代码时,都会假定对这些代码的调用是通过JavaScript完成的,而与Ajax相关的安全性的一个要点是,恶意用户可以直接调用Ajax的服务器端代码(例如,可以使用命令行工具比如curl或者wget,或者直接在浏览器中输入该URL)。你需要从这两个方面来对你的代码进行测试。
 

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 表单API的安全性

使用表单API的一个好处就是,它为你处理了许多安全性问题。例如,Drupal通过检查来保证,用户从下拉选择框中选择的值,确实是Drupal生成的选项中的一个。表单API使用了一系列的事件集,比如表单构建、验证、和执行。在验证阶段前面,你不能够使用用户输入,因为用户输入还没有被验证。例如,如果你使用的值来自$_POST,那么你就不能确保用户是否操作了该值。还有就是,使用#value元素在表单中传递信息,尽可能的使用它来代替隐藏域,因为恶意用户可以操作隐藏域,但是访问不了#value元素。

对于用于构建表单的用户提交的数据,也需要进行适当的安全清理,这和用户提交的其它数据一样,如下所示。
不安全:
 
$form['foo'] = array(
'#type' => 'textfield',
'#title' => $node->title, // XSS vulnerability!
'#description' => 'Teaser is: '. $node->teaser, // XSS vulnerability!
'#default_value' => check_plain($node->title), // Unnecessary.
);
 
安全:
 
$form['foo'] = array(
'#type' => 'textfield',
'#title' => check_plain($node->title),
'#description' => t('Teaser is: @teaser', array('@teaser' => $node->teaser)),
'#default_value' => $node->title,
};
 
对于默认值,没有必要使用check_plain(),这是因为表单元素类型的主题函数实现了这一点(在这里,为includes/form.inc中的theme_textfield()函数)。
 
警告 如果你使用自己的主题函数来覆写Drupal的默认主题函数,那么你需要检查一下,在默认的主题函数中是不是对一些用户输入作了安全处理,如果有的话,在你的代码里,也需要实现这一点。
 
更多关于表单API的信息参看第10章。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 保护超级用户帐号

获取一个Drupal网站密码的最简单的方式是,打个电话给这个网站的秘书,这样说“你好,我是Joe。<一对客套话>。我是给你们的网站提供技术支持的,在技术支持中,我们遇到了问题。需要使用用户名和密码进行登录,你的用户名和密码是多少?”不幸的是,很多人会轻易的将这一私密信息告诉他人。尽管技术对此有所帮助,但是对于这种攻击,最好的办法还是对用户进行教育。

正因为此,最好不要将超级用户(用户1)授予任何人。对于任何维护网站的用户,只能授予他完成任务所需要的权限。这样,即使出现了安全问题,危害也是可以控制的。

老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 使用eval()

千万不要使用它。使用PHP函数eval(),你可能觉得它是进行元编程的极好的方式,或者想用它来减少多行代码,这个函数将一个字符串文本作为输入,并使用PHP解释器对其求值。这完全是一个错误。如果有任何方式允许一个用户使用eval()来操作输入的话,那么你就会将PHP解释器的威力暴露给用户。这距离泄露私密数据的时间也不会太长了,因为用户就可能使用这一方式来获取你的数据库中的用户名和密码。

这也是为什么你只能在Drupal后台管理接口中使用PHP过滤器的原因,而且只有在非常特定的环境中并具有相关授权的前提下才使用它。为了能够睡个安稳觉,避免使用eval()和PHP过滤器。Drupal的确在其内核中使用了eval(),但是情况非常少,并且使用了drupal_eval()对其进行封装,drupal_eval()可以阻止要进行估值的代码,覆盖调用drupal_eval()的代码中的变量。drupal_eval()位于includes/common.inc。
 老葛的Drupal培训班 Think in Drupal

Drupal版本:

Drupal专业开发指南 第20章 总结

读完本章后,你应该知道:

    • 永远不要相信用户的输入
    • 如何将用户的输入转换为安全的形式
    • 如何阻止XSS攻击
    • 如何阻止SQL注入攻击
    • 如何编写考虑了节点访问模块的代码
    • 如何阻止CSRF攻击
    • Drupal是如何保护上传文件的
    • 如何阻止e-mail头部注入

老葛的Drupal培训班 Think in Drupal

Drupal版本: