衆所周知,web領域近年來安全問題日益被看中,而首要問題就是所謂【XSS攻擊】。
聽一老師說,這非常重要,甚至可以輕而易舉的獲取此網站上的所有信息,和權限。今早試了一下,果然如此。筆者覺得有必要以此爲例寫篇文章來和諸位分析一下…
XSS的“前世今生”
XSS原理: XSS攻擊是Web攻擊中最常見的攻擊方法之一,它是通過對網頁注入可執行代碼且成功地被瀏覽器 執行,達到攻擊的目的,形成了一次有效XSS攻擊,一旦攻擊成功,它可以獲取用戶的聯繫人列表,然後向聯繫人發送虛假詐騙信息,可以刪除用戶的日誌等等,有時候還和其他攻擊方式同時實 施比如SQL注入攻擊服務器和數據庫、Click劫持、相對鏈接劫持等實施釣魚,它帶來的危害是巨 大的,是web安全的頭號大敵。
攻擊條件:
- 需要向web頁面注入惡意代碼;
- 這些惡意代碼能夠被瀏覽器成功的執行
XSS攻擊注入點
- HTML節點內容
- HTML屬性(最常見的比如:
<img src="null" onerror="alert('1')" />
) - js代碼
- 富文本
XSS攻擊與防禦手段
- 反射型
- 存儲型
反射型XSS攻擊: XSS代碼在URL中隨輸入提交(請求)到服務器端,服務器端解析後響應。XSS代碼隨響應內容一起回到瀏覽器,被執行。
這是一個明文攻擊,或者,常表現爲“誘導型攻擊”。
存儲型XSS攻擊: 他和反射型攻擊唯一的區別在於代碼存儲地方。存儲型XSS,其提交代碼會被存儲在服務端(數據庫、內存。文件系統…)
# XSS防禦(黑名單 & 白名單) #
- 編碼 ——對用戶輸入的數據進行HTML Entity編碼:
'' - "、& - &、< - <、> - >、不斷開空格 -
(前面的是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://*;">
圖樣
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,">");
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】部分是從後端拿到數據的展示過程,但在此之前,有兩個步驟:
- 解碼
- 配對校驗
筆者在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】。