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】。

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