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

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

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


XSS的“前世今生”

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

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

XSS攻击注入点

  • 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标签

# 浏览器防御: #
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://*;">


图样

xss1

xss2


XSS实战

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

mkdir mxcyun
cd mxcyun/
npm install

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

启动服务命令:

cd mxcyun/
npm start

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

此次服务器端所用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,"&gt");
	s=str,replace(/</g,"&lt;");
	s=str,replace(/>/g,"&gt;");
	s=str,replace(/\s/g,"&nbsp;");
	s=str,replace(/\'/g,"&#39;");
	s=str,replace(/\"/g,"&quot;");
	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】。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章