安全的文本处理方式
http://drupalchina.cn/node/1587
最后更新: 2012年4月1日. 由 perusio 于2005年8月16日创建 编辑: klausi, greggles, dkinzer, HongPong. 登陆以编辑此页.
当处理并输出文本到HTML的时候,你需要格外注意这些文本是不是经过了适当的过滤和换码。否则当用户使用尖括号或者”&”字符的时候可能会引发Bug,更糟的情况是可能引发XSS攻击。
处理数据的黄金法则是准确存储用户的输入。当用户编辑一个他们已经创建的内容,表单的内容应该和他们第一次提交的内容一模一样。 这意味着转换不是在内容存入数据库的时候进行的,而是在输出的时候进行的(注意仔细阅读db_query() 文档,了解如何安全得使用数据库API)。
为了方便知道文本的哪些地方需要检查,要做到能一眼看出一个字符串用的是什么格式(纯文本,HTML,BB代码或是Textile)。当你连接两个字符串的时候,你必须确保他们的格式是一样的。 如果他们的格式不同就必须进行适当的检查转换或者过滤。
用户在Drupal中提交的数据可以分为3类:
- 纯文本
这是没有任何标记的简单文本。用户的输入会被完整准确地输出到屏幕,并且不会被解读为其他形式。它一般被用于单行文本框。
在将纯文本输出到HTML之前,你需要用函数check_plain()进行检查。 这个函数会将引号,”&”和尖括号转换为字符实体,保证字符输出的时候保持原样。
大多数主题函数和API没有经过check_plain() 检查就将HTML作为参数直接使用:
- t():纯文本中的占位符 (比如 '%name' or '@name')在插入可转换字符串时会被换码。 你可以使用’!name’这种格式来避免被换码(了解更多).
- l(): 连接标题应该以纯文本形式传递(除非被 $htmlparameter覆盖).
- 菜单项和导航: 菜单项标题和导航标题会自动进行检查。
- theme('placeholder'): 占位符(placeholder)是纯文本
- 块描述 (不是标题—详见下面)
- 使用 theme_username()显示用户名
- 表单 API (FAPI) 下拉列表的#default_value元素和#options元素 例子:
<?php $form['safe'] = array( '#type' => 'textfield', '#default_value' => $u_supplied, ); $form['also_safe'] = array( '#type' => 'select', '#default_value' => 0, // FAPI 会调用 check_plain()进行检查, '#options' => node_get_types('names'), // FAPI 会调用check_plain()检查下拉列表的 '#options' 属性,并替换有害内容 ); // 在Drupal7中通过 filter_xss_admin()函数的内容是XSS安全的(不会形成跨域攻击). $form['drupal6_unsafe'] = array( '#type' => 'checkboxes', '#default_value' => 0, // FAPI 会调用check_plain()进行检查, '#options' => node_get_types('names'), // FAPI 不会对除了下拉列表外的'#options' 属性进行无害化 ); ?>
有些需要你首先进行无害化的地方:
- 通过 drupal_set_title()设定的页面标题.页面标题会在HTML中显示,并且可以使用一些标签例如 <em> 使其看起来更清晰. 但是如果页面标题在HTML标签中显示,标题包含的标签都应该无效化. 例子:
drupal_set_title($node->title); // XSS 漏洞, 不好 drupal_set_title(check_plain($node->title)); // 正确
- 块标题通过 hook_block()创建. 和页面标题一样,这里通常会使用一些HTML标签。
- 监视器(watchdog)消息 例子:
Drupal 6/7 (在监视器(watchdog)函数中信息和变量通过 t() 来传递): watchdog('content', "Deleted !title", array('!title' => $node->title)); // XSS watchdog('content', "Deleted %title", array('%title' => $node->title)); // 或者使用 @来代替%
- 表单元素 #description 和 #title 例子:
$form['bad'] = array( '#type' => 'textfield', '#default_value' => check_plain($u_supplied), // 不好: 换码两次 '#description' => t("Old data: !data", array('!data' => $u_supplied)), // XSS ); $form['good'] = array( '#type' => 'textfield', '#default_value' => $u_supplied, '#description' => t("Old data: @data", array('@data' => $u_supplied)), );
- 仅适用于Drupal 6:表单元素- #options, 当 #type = checkboxes 或#type = radios时 例子:
// 在Drupal 7+ XSS安全. $form['drupal6_bad'] = array( '#type' => 'checkboxes', '#options' => array($u_supplied0, $u_supplied1), ); $form['good'] = array( '#type' => 'checkboxes', '#options' => array(check_plain($u_supplied0), check_plain($u_supplied1)), );
- 表单元素- #type 的 #value 标签和项应该是安全的. 注意默认表单元素#type是标签! 例子:
$form['unsafe'] = array('#value' => $user->name); //XSS $form['safe'] = array('#value' => check_plain($user->name)); 或 $form['safe'] = array('#value' => theme('username', $user));
- 多信息文本
有一些语言可以标记这种文本(HTML, Textile, 等等).用特定的标记格式来储存这种文本,用各种各样的过滤转换为HTML进行输出。 这种格式多用于多行文本框。
你只需要把多信息文本传递给 check_markup() 就能返回安全的HTML. 你应该同时允许用户通过使用filter_form()的格式插件选择输入格式,并把选择的格式一并传递给check_markup() 。
注意你必须确保编辑者能够使用特定的输入格式。作为一种防护措施, check_markup()默认会检查当前用户。然而由于内容是在输出的时候进行过滤,所以内容原作者经常不是当前用户。这时候就必须把$check=false传递给 check_markup()来关闭检查功能,但必须确保数据格式在提交的时候经过filter_access() 的检查。
- 管理用 HTML
从 Drupal 4.7 其开始有第三种文本处理方式。 在一些和管理相关的地方使用过滤系统是不切实际的 (如多信息文本), 但是这些地方放需要用一些简单简单的标签, 比如超链接或是强调 (所以不该用纯文本).
例子包括任务说明,发布指南,论坛描述。
在这些情况下, 你可以使用一般的文本域,并在输出的时候将其传递给filter_xss_admin() 进行过滤. 这个函数除了去掉一些可能会造成危害的脚本和样式会保留绝大多数HTML标签.
URL 需要用以下两种方式进行特殊处理:
- 如果你的URL包含任何动态数据, 则要把它传递给urlencode(). 否则类似”#”和”?”的字符会被转换成URL语义而被破坏.urlencode() 能够避免类似 %XX 语法被转码. 主义 Drupal 路径 (如 'node/123') 从Drupal4.7开始整个被传递给 urlencode() ,所以你不必每个部分都单独进行url编码。GET请求的参数和片段标识符不能享受这种便利.
- 在超链接中使用用户提交的URL,则还需要用 check_url() 而不是 check_plain(). check_url()会调用 check_plain(), 还会进行一些额外的XSS检查。注意所有返回URL的Drupal函数(url(), request_uri(), 等等)输出已经对HTML进行换码的纯URL(或者说,返回的是纯文本)。在输出HTML或XML的时候记得使用check_url()进行换码。在需要一个真正的URL的场合则不要用check_url(),比如HTTP Location:…header。
练习
上面所讲述的所有规则总结起来很容易: 所有用户提交的内容不能按原样输出到HTML。如果你不确定文本是不是按原样输出,你可以在你的模块域里面添加像<u>xss</u>的文本来测试。如果这个文本有下划线或是搞乱了其他的标签,你就知道这里有问题。
这里有一些好的和不好的例子。 把$title,$body和 $url假定为分别包含标题,标记文本和URL的用户提交的字段。他们在数据库里面的数据和用户提交的数据是一模一样的。
不好的例子: <?php print '<tr><td>$title</td><td>'; ?> <?php print '<a href="/..." title="$title">view node</a>'; ?>
好的例子 (标题应该是纯文本不包含HTML标签): <?php print '<tr><td>'. check_plain($title) .'</td></tr>'; ?> <?php print '<a href="/..." title="'. check_plain($title) .'">view node</a>'; ?>
不好的例子: <?php print l(check_plain($title), 'node/'. $nid); ?>
好的例子 (l() 默认会调用 check_plain() ): <?php print l($title, 'node/'. $nid); ?>
不好的例子: <?php print '<a href="/$url">'; ?> <?php print '<a href="/'. check_plain($url) .'">'; ?>
好的例子 (URL必须经过 check_url()的检查): <?php print '<a href="/'. check_url($url) .'">'; ?>
编写过滤器
编写一个过滤器来把其他标记语言转换为HTML。你要确保自己不要留下漏洞。通常也和输出时候一样要用check_url()来检查URL,用check_plain().来确保不被HTML注入。
评论
下拉框已经默认进行安全检查
由 stewart.adam 发表于 December 18, 2011 at 8:23am
下拉框在Drupal6.22以后核心会默认进行安全检查 (早起版本可能也有,不过没有进行测试). 在 includes/form.inc, form_select_options() 函数, 1452行所有数据默认经过check_plain()检查:
<?php $options .= '<option value="'. check_plain($key) .'"'. $selected .'>'. check_plain($choice) .'</option>'; ?>
filter_xss()这个函数没有涉及吗?
由 cigotete 发表于 March 22, 2012 at 9:15pm
我很好奇 filter_xss() 函数的具体功能, 它不用来进行安全检查吗?