众所周知,web领域近年来安全问题日益被看中,而首要问题就是所谓【XSS攻击】。

听一老师说,这非常重要,可以轻而易举的获取此网站上的所有信息,和权限。今早试了一下,果然如此。笔者觉得有必要以此为例写篇文章来和诸位分析一下…


XSS的“前世今生”

XSS原理: XSS攻击是Web攻击中最常见的攻击方法之一,它是通过对网页注入可执行代码且成功地被浏览器 执行,达到攻击的目的,形成了一次有效XSS攻击,一旦攻击成功,它可以获取用户的联系人列表,然后向联系人发送虚假诈骗信息,可以删除用户的日志等等,有时候还和其他攻击方式同时实 施比如SQL注入攻击服务器和数据库、Click劫持、相对链接劫持等实施钓鱼,它带来的危害是巨 大的,是web安全的头号大敌。
攻击条件:

  1. 需要向web页面注入恶意代码;
  2. 这些恶意代码能够被浏览器成功的执行

XSS攻击注入点

  • url
  • HTML节点内容
  • HTML属性(最常见的比如:​​<img src="null" onerror="alert('1')" />​​)
  • js代码
  • 富文本

XSS攻击与防御手段

  • 反射型
  • 存储型

反射型XSS攻击: XSS代码在URL中随输入提交(请求)到服务器端,服务器端解析后响应。XSS代码随响应内容一起回到浏览器,被执行。
这是一个明文攻击,或者,常表现为“诱导型攻击”。
存储型XSS攻击: 他和反射型攻击唯一的区别在于代码存储地方。存储型XSS,其提交代码会被存储在服务端(数据库、内存。文件系统…)

# XSS防御(黑名单 & 白名单) #

  • 编码 ——对用户输入的数据进行HTML Entity编码:​​'' - &quot;、& - &amp;、< - &lt;、> - &gt;、不断开空格 - &nbsp;​​ (前面的是HTML内容,后面是编码成什么样子)
  • 过滤(配对校验) —— 1、移除用户上传的DOM属性,如:​​onerror​​​; 2、移除用户上传的style节点、​​script​​​节点、​​Iframe​​​节点、​​frame​​​节点、​​link​​​节点… ——比如这样:​​if(tag==’ … ’ || …) return;​
  • 校正 —— 避免直接对HTML Entity编码,使用DOM Parse转换,校正不配对的DOM标签

# 浏览器防御: #
cookie的HTTPOnly字段,格式为:​​response.setHeader( "Set-Cookie" , "cookiename=httponlyTest;Path=/;Domain=domainvalue;Max-Age=seconds;HTTPOnly");​​​,由后端设置。如果cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息
X-XSS-Protection头,它有三种状态:0——关闭浏览器XSS防御;1——打开浏览器XSS防御(默认);1+url——打开指定url的XSS防御
不过有一点:这种机制反应最为“粗暴”,防御范围也非常小(防“反射型XSS”、“节点/属性中出现的脚本”),不可靠
Content-Security-Policy头,格式为:​​Content-Security-Policy:default-src 'self' ...​​。总之各种【-src】——用于限制网站资源来源,常见如:

  • 想要所有内容均来自站点的同一个源 (不包括其子域名):​​Content-Security-Policy: default-src 'self'​​(这个就可以作为本文的一个范例:防反射型XSS攻击)
  • 允许内容来自信任的域名及其子域名 (域名不必须与CSP设置所在的域名相同):​​Content-Security-Policy: default-src 'self' *.trusted.com​
  • 一个在线邮箱的管理者想要允许在邮件里包含HTML,同样图片允许从任何地方加载,但不允许JavaScript或者其他潜在的危险内容(从任意位置加载):​​Content-Security-Policy: default-src 'self' *.cjxnsb.c; img-src *​

这个头的特别之处在于:它还可以放在HTML的meta标签里:​​<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*;">​


图样

Web安全:细说前端XSS攻击与防范_es6

Web安全:细说前端XSS攻击与防范_javascript_02


XSS实战

首先,创建一个目录,并进入、运行(node.js):

mkdir mxcyun
cd mxcyun/
npm install

cd ../
open mxcyun -a HBuilder #用HBuilder打开此目录(mxcyun)

启动服务命令:

cd mxcyun/

(启动服务后即可在相应网址查看效果!后面所用到的也是这个命令)

此次服务器端所用node.js ,中的express插件(模块)(主要是其中的Router中间件)!客户端所用为domParse.js插件(提供HTMLParse)和encode.js插件(提供he)。

npm install express -g

前端所用插件下载地址:
domParse.js => ​​​https://github.com/blowsie/Pure-JavaScript-HTML5-Parser​​​ encode.js => ​​https://github.com/mathiasbynens/he​

//node.js代码
var express=require('express');
var router=express.Router();

router.get('/',function(req,res,next){
res.render('index',{title:'Express'});
});

module.exports=router;

(在其中)先构造两个接口 —— 接收输入和返回文字:

//node.js代码-接收接口部分
var comments={};
router.get('/comment',function(req,res,next){
comments.v=req.query.comment;
})

这个接口的作用即为【保存输入内容】,但是这就够了么?
我们前面才说过编码的问题:

//node.js代码-“编码”函数部分
function html_encode(str){
var s='';
if(s.length==0) return ""
s=str,replace(/&/g,">");
s=str,replace(/</g,"<");
s=str,replace(/>/g,">");
s=str,replace(/\s/g," ");
s=str,replace(/\'/g,"'");
s=str,replace(/\"/g,""");
s=str,replace(/\n/g,"<br>");
return s;
};

所以接收部分代码应改为:

//node.js代码-接收接口部分
var comments={};
router.get('/comment',function(req,res,next){
comments.v=html_encode(req.query.comment);
});

那么,

//node.js-用户拉取(获取)评论接口部分
router.get('/getComment',function(req,res,next){
res.json({
comment:comments.v
})
})

让我们把目光聚焦到前端部分:

<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/style/style.css" />
<script src="/javascript/encode.js"></script>
<script src="/javascript/domParse.js"></script>
</head>
<body>
<textarea name="name" rows="8" cols="80" id="txt">
<p>sks <img src="null" onerror="alert(1)"></p>
</textarea>
<button type="button" name="button" id="btn">评论</button>
<button type="button" name="button" id="get">获取评论</button>
</body>
</html>

如上,前端部分所用为node.js中的ejs模板(创建的项目的view目录下),如果是普通HTML文件,则需在服务器node文件中加入path模块定位前端资源:

var path=require('path');
var app=express();

app.use(express.static(path.join(__dirname+'/public')))
app.get('/',function(req,res){
res.sendFile(path.join(__dirname+'/public/index.html'));
})

或直接用:

router.get('/',function(req,res,next){
res.sendFile(path.join(__dirname+'/public/index.html'));
})

下面来写整个的交互部分:

<script>
btn.addEventListener('click',function(){
var xhr=new XMLHttpRequest();
var url='/comment?comment='+txt.value;
xhr.open('GET',url,true);
xhr.onreadystatechange=function(){
if(xhr.readyState==4 && xhr.status==200){
console.log(xhr);
}else{
console.log('error');
}
}
xhr.send();
});
get.addEventListener('click',function(){
var xhr=new XMLHttpRequest();
var url='/getComment';
xhr.open('GET',url,true);
xhr.onreadystatechange=function(){
if(xhr.readyState==4 && xhr.status==200){
//注释1
}else{
console.log('error');
}
}
xhr.send();
});
</script>

代码中【注释1】部分是从后端拿到数据的展示过程,但在此之前,有两个步骤:

  1. 解码
  2. 配对校验

笔者在ejs文件​​head​​中又写了一个script标签 —— 其中放置的是解码和配对的函数:

<script>
var parse=function(str){
var results='';
//为防止错误,将过程放在try-catch中进行
try{
HTMLParse(he.unescape(str,{strict:true}),{
//HTMLParse提供了几个内置选项
//标签的开始部分(标签,属性,是不是单标签)
start:function(tag,attrs,unary){
//在start中过过滤掉不安全的标签元素
if(tag=='script' || tag='style' || tag=='link' || tag=='iframe' || tag=='frame') return;
if(tag=='img'){
for(var i in attrs){
if(attrs[i].name=='src'){
results+=" "+attrs[i].escaped;
}
}
return results;
}
results+='<'+tag;

results+=(unary?"/":"")+">";
},
//标签的结束部分
end:function(tag){
results+="</"+tag+">";
},
//中间的文本部分
chars:function(text){
results+=text;
},
//处理其中的注释部分
comment:function(text){
results+="<!--"+text+"-->"
}
});
return results;
}catch(e){
console.log(e);
}finally{}
}
</script>

HTMLParse函数时domParese第三方插件的内置函数,就是为解决反转义问题,其中unescape的第一个参数就是文本/html片段,第二个参数是“使用严格模式”,而he是HTMLParse这个函数的一个(负责此块功能的)内置对象。

然后我们将回到上一个代码【注释1】部分:

var com=parse(JSON.parse(xhr.response).comment);
var txt=document.createElement('span');
txt.innerHTML=com; //这里为什么用HTML?因为com已经是转移之后的内容了
document.body.appendChild(txt);

总的来说就是,后端编码,前端转义(过滤和校正)


上面一段的防御方法又俗称【黑名单】,这一方法十分简便,但是有一个缺点就是:在大型项目中“力度”不够 —— HTML标签众多,如果靠黑名单来阻止某些“不法操作”的话,怕是要么凉凉,要么增加HTML解析难度。

所以,我们还可以通过【白名单】的方式处理:设置允许通过的标签(服务端设置):

cnpm install cheerio -S

cheerio是nodeJS中一个和jQuery用法极其类似的模块,用于获取和操作dom元素

var html_encode=function(html){
if(!html) return '';
var cheerio=require('cheerio');
var $=cheerio.load(html);
var whiteList={
'img':['src'],
'font':['color','size'],
'a':['href']
};
$('*').each(function(index,elem){
if(!whiteList[elem.name]){
$(elem).remove();
return;
}
for(var attr in elem.attribs){
if(!whiteList[elem.name].includes(attr)){ //重点!
$(elem).attr(attr,null);
}
}
});
return $.html();
};

总结来说,就是【不在白名单中的元素(从dom树中)删除,不在白名单中的属性赋值为null】。