description: 这里包含了xss的基本定义,原理、及防御方案代码示例。
简介
跨站脚本XSS是一种注入类型攻击,攻击者注入恶意代码到web应用程序并发送给不同的最终用户时,就会发生xss攻击,并且可能发生在web应用程序在其生成的输出中使用来之用户的输入并且无需验证或编码它的任何地方。
跨站脚本发生在:
- 任何可以插入数据,并且数据包会未经恶意内容验证就发送给Web用户的地方。
跨站脚本分为两类:
- 反射型(又称非持久性xss): 反射性xss通常出现在错误消息、搜索结果、或者服务器实时响应部分或全部请求的地方。可以通过电子邮件、恶意链接、等途径进行攻击。
- 存储型(又称持久性xss): 存储型xss通常将恶意代码永久存储在目标服务器上,如数据库、消息论坛、访问日志、注释字段等。
- DOM型: dom型xss我觉得也属于存储型,只不过是存储在客户端。有效载荷修改了客户端浏览器中的DOM环境并执行恶意代码,也就是说HTTP响应是不变的,恶意代码被存储在客户端响应页面中。因此很难在服务器端做安全检测。
危害
xss可以窃取用户的会话cookie,允许攻击者劫持用户的会话并接管账号,其他攻击包括最终用户文件的泄露、木马程序安装、将用户重定向到其他页面或站点、修改内容的呈现等。
具体危害如:允许攻击者修改新闻稿或新闻项目的XSS漏洞可能会影响公司的股价或降低消费者信心。制药网站上的XSS漏洞可能允许攻击者修改剂量信息,从而导致过量服用。
防御
1、在输出时将不可信数据(用户可控的数据:包括主体、属性、JavaScript、CSS或URL)使用 htmlEscape()进行转义;将html、js中涉及到的关键字符进行html编码处理后再输出到页面上。
2、如果存在用户需要编写文章等功能的时候,类似用户可以自定义页面样式的功能,则需要限制用户的输入,采用严格的白名单机制来过滤用户的输入,即非用户允许的功能标签都应该被完全过滤,这种过滤应依赖于后端应用来实现。
3、应注意很多开源的第三方富文本编辑器,部分编辑器仅在JS端进行过滤,而不在后端进行关键字过滤。
代码示例
1.Java
Portal 或 Spring 框架修复方案:
import org.springframework.web.util.HtmlUtils;
target = HtmlUtils.htmlEscape(target);
// target 参数为需要进行转义的参数,建议在该参数输出前进行转义
通用型修复方案:
StringEscapeUtils from Apache Commons Lang:
import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
// ...
String source = "The less than sign (<) and ampersand (&) must be escaped before using them in HTML";
String escaped = escapeHtml(source);
For version 3:
import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4;
// ...
String escaped = escapeHtml4(source);
2.PHP
htmlspecialchars() 在输出时使用该函数转义用户输入的值,尤其注意第二个 flag 参数要使用 ENT_QUOTES
<?php
$name = $_GET["name"];
$name = htmlspecialchars($name, ENT_QUOTES);
?>
htmlentities() 在输出时使用该函数转义用户输入的值
针对富文本,使用以下过滤类进行处理:
<?php
/**
* PHP 富文本XSS过滤类
*
* @package XssHtml
* @version 1.0.0
* @link http://phith0n.github.io/XssHtml
* @since 20140621
* @copyright (c) Phithon All Rights Reserved
*
*/
#
# Usage:
# <?php
# require('Xss.php');
# $html = '<html code>';
# $xss = new XssHtml($html);
# $html = $xss->getHtml();
# ?\>
#
# 需求:
# PHP Version > 5.0
# 浏览器版本:IE7+ 或其他浏览器,无法防御IE6及以下版本浏览器中的XSS
# 更多使用选项见 http://phith0n.github.io/XssHtml
class Xss {
private $m_dom;
private $m_xss;
private $m_ok;
private $m_AllowAttr = array('title', 'src', 'href', 'id', 'class', 'style', 'width', 'height', 'alt', 'target', 'align');
private $m_AllowTag = array('img', 'br', 'strong', 'b', 'code', 'p', 'div', 'em', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'table', 'ul', 'ol', 'tr', 'th', 'td', 'hr', 'li', 'u');
/**
* 构造函数
*
* @param string $html 待过滤的文本
* @param string $charset 文本编码,默认utf-8
* @param array $AllowTag 允许的标签,如果不清楚请保持默认,默认已涵盖大部分功能,不要增加危险标签
*/
public function __construct($html, $charset = 'utf-8', $AllowTag = array()){
$this->m_AllowTag = empty($AllowTag) ? $this->m_AllowTag : $AllowTag;
$this->m_xss = strip_tags($html, '<' . implode('><', $this->m_AllowTag) . '>');
if (empty($this->m_xss)) {
$this->m_ok = FALSE;
return ;
}
$this->m_xss = "<meta http-equiv=\"Content-Type\" content=\"text/html;charset={$charset}\"><nouse>" . $this->m_xss . "</nouse>";
$this->m_dom = new DOMDocument();
$this->m_dom->strictErrorChecking = FALSE;
$this->m_ok = @$this->m_dom->loadHTML($this->m_xss);
}
/**
* 获得过滤后的内容
*/
public function getHtml()
{
if (!$this->m_ok) {
return '';
}
$nodeList = $this->m_dom->getElementsByTagName('*');
for ($i = 0; $i < $nodeList->length; $i++){
$node = $nodeList->item($i);
if (in_array($node->nodeName, $this->m_AllowTag)) {
if (method_exists($this, "__node_{$node->nodeName}")) {
call_user_func(array($this, "__node_{$node->nodeName}"), $node);
}else{
call_user_func(array($this, '__node_default'), $node);
}
}
}
$html = strip_tags($this->m_dom->saveHTML(), '<' . implode('><', $this->m_AllowTag) . '>');
$html = preg_replace('/^\n(.*)\n$/s', '$1', $html);
return $html;
}
private function __true_url($url){
if (preg_match('#^https?://.+#is', $url)) {
return $url;
}else{
return 'http://' . $url;
}
}
private function __get_style($node){
if ($node->attributes->getNamedItem('style')) {
$style = $node->attributes->getNamedItem('style')->nodeValue;
$style = str_replace('\\', ' ', $style);
$style = str_replace(array('&#', '/*', '*/'), ' ', $style);
$style = preg_replace('#e.*x.*p.*r.*e.*s.*s.*i.*o.*n#Uis', ' ', $style);
return $style;
}else{
return '';
}
}
private function __get_link($node, $att){
$link = $node->attributes->getNamedItem($att);
if ($link) {
return $this->__true_url($link->nodeValue);
}else{
return '';
}
}
private function __setAttr($dom, $attr, $val){
if (!empty($val)) {
$dom->setAttribute($attr, $val);
}
}
private function __set_default_attr($node, $attr, $default = '')
{
$o = $node->attributes->getNamedItem($attr);
if ($o) {
$this->__setAttr($node, $attr, $o->nodeValue);
}else{
$this->__setAttr($node, $attr, $default);
}
}
private function __common_attr($node)
{
$list = array();
foreach ($node->attributes as $attr) {
if (!in_array($attr->nodeName,
$this->m_AllowAttr)) {
$list[] = $attr->nodeName;
}
}
foreach ($list as $attr) {
$node->removeAttribute($attr);
}
$style = $this->__get_style($node);
$this->__setAttr($node, 'style', $style);
$this->__set_default_attr($node, 'title');
$this->__set_default_attr($node, 'id');
$this->__set_default_attr($node, 'class');
}
private function __node_img($node){
$this->__common_attr($node);
$this->__set_default_attr($node, 'src');
$this->__set_default_attr($node, 'width');
$this->__set_default_attr($node, 'height');
$this->__set_default_attr($node, 'alt');
$this->__set_default_attr($node, 'align');
}
private function __node_a($node){
$this->__common_attr($node);
$href = $this->__get_link($node, 'href');
$this->__setAttr($node, 'href', $href);
$this->__set_default_attr($node, 'target', '_blank');
}
private function __node_embed($node){
$this->__common_attr($node);
$link = $this->__get_link($node, 'src');
$this->__setAttr($node, 'src', $link);
$this->__setAttr($node, 'allowscriptaccess', 'never');
$this->__set_default_attr($node, 'width');
$this->__set_default_attr($node, 'height');
}
private function __node_default($node){
$this->__common_attr($node);
}
}
?>