web安全防范之XSS漏洞攻擊
最近在cnode社區,由@吳中驊的一篇關于XSS的文章,直接導致了社區的人開始在cnode嘗試各種攻擊。這里總結了一下這次碰到的一些問題與解決方案。
文件上傳漏洞
之前nodeclub在上傳圖片的時候邏輯是這樣的:
- //用戶上傳的文件名
- var filename = Date.now() + '_' + file.name;
- //用戶文件夾
- var userDir = path.join(config.upload_dir, uid);
- //最終文件保存的路徑
- var savepath = path.join(userDir, filename);
- //將用戶上傳的文件從臨時目錄移動到最終保存路徑
- fs.rename(file.path, savepath, callback);
看上去好像沒有問題,每個人上傳的文件都存放在以用戶UID命名的一個文件夾內,并且以當前的時間戳作前綴。但是當有用戶惡意構造輸入的時候,問題就出現了。當用戶上傳的文件filename為/../../xxx的時候,上傳的文件就會rename到用戶文件夾之外,導致用戶可以替換現有系統上的任何文件。
這個漏洞相對來說非常的低級,但是后果卻是最嚴重的,直接導致整個系統都可能被用戶控制。修復的方法也很簡單:
- var filename = Date.now() + '_' + file.name;
- var userDir = path.join(config.upload_dir, uid);
- //獲取最終保存到的絕對路徑
- var savepath = path.resolve(path.join(userDir, filename));
- //驗證
- if (savepath.indexOf(path.resolve(userDir)) !== 0) {
- return res.send({status: 'forbidden'});
- }
- fs.rename(file.path, savepath, callback);
富文本編輯器的XSS
關于XSS,在@吳中驊的文章中已經非常詳細的描述了。而cnode社區中,用戶發表話題和回復話題也是用的一個支持markdown格式的富文本編輯器。之前是沒有做過任何XSS防范措施的,于是...你可以直接在里面寫:
- <script>alert(123);</script>
- <div onmouseover="alert(123)"></div>
- <a href="javascript:alert(123);">123</a>
而markdown格式的內容也沒有做URL有效性檢測,于是各種樣式的XSS又出來了:
[xss][1]
[xss][2]
![xss][3]
[1]: javascript:alert(123);
[2]: http://www.baidu.com/#"onclick='alert(123)'
[3]: http://www.baidu.com/img.jpg#"onmouseover='alert(123)'
在社區這個應用場景下,引入HTML標簽只是為了進行一些排版的操作,而其他的樣式定義等等都只會讓整個界面一團糟,更別說還有潛在的XSS漏洞風險。因此,其實我們是不需要支持用戶輸入HTML標簽來進行內容排版的,一切都可以通過markdown來代替。然后通過簡單粗暴的HTML escape,就可以消滅掉直接輸入HTML導致的XSS風險。
- function escape(html) {
- return html.replace(/&(?!\w+;)/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"');
- }
然而這樣粗暴的進行escape,會導致用戶輸入的代碼里面的< > ;這些特殊字符也被轉義掉,不能正確顯示,需要先將代碼段提取出來保存,只轉義非代碼段的部分。于是這個escape函數變成了這樣:
- function escape(html) {
- var codeSpan = /(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm;
- var codeBlock = /(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g;
- var spans = [];
- var blocks = [];
- var text = String(html).replace(/\r\n/g, '\n')
- .replace('/\r/g', '\n');
- text = '\n\n' + text + '\n\n';
- texttext = text.replace(codeSpan, function(code) {
- spans.push(code);
- return '`span`';
- });
- text += '~0';
- return text.replace(codeBlock, function (whole, code, nextChar) {
- blocks.push(code);
- return '\n\tblock' + nextChar;
- })
- .replace(/&(?!\w+;)/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"')
- .replace(/`span`/g, function() {
- return spans.shift();
- })
- .replace(/\n\tblock/g, function() {
- return blocks.shift();
- })
- .replace(/~0$/,'')
- .replace(/^\n\n/, '')
- .replace(/\n\n$/, '');
- };
而對于markdown生成的<a>標簽和<img>標簽中的href屬性,必須要做URL有效性檢測或者做xss的過濾。這樣保證通過markdown生成的HTML代碼也是沒有XSS漏洞的。
因為XSS的手段確實比較多,見XSS Filter Evasion Cheat Sheet。因此能夠做粗暴的HTML escape是最安全的,但是并不是每一個地方都可以通過markdown來代替HTML代碼,所以不是每一個地方都能用HTML escape,這個時候就需要其他的手段來過濾XSS漏洞了。
XSS防范只能通過定義白名單的形式,例如只允許<p> <div> <a>標簽,只允許href class style屬性。然后對每一個可能造成XSS的屬性進行特定的過濾。
現有的XSS過濾模塊,一個是node-validator, 一個是@雷宗民寫的js-xss。
不能夠保證XSS模塊可以防范住任意的XSS攻擊,但是起碼能夠過濾掉大部分能夠想象到的漏洞。node-validator的XSS()仍然有bug,對于<p on="></p>形式的代碼,會有雙引號不閉合的問題,導致HTML元素測漏。
模版引擎導致的XSS攻擊
cnode社區采用的是ejs作為模版引擎,而在ejs中,提供了兩種輸出動態數據到頁面的方法:
<% =data %> //進行xss過濾的輸出
<% -data %> //不過濾直接輸出
而所有的過濾必須有一個前提: 模版文件中的HTML屬性的值等,必須使用雙引號。 例如:
- <img src='<%= reply.author.avatar_url %>' title='<%= reply.author.name %>' />
- <img src="<%= reply.author.avatar_url %>" title="<%= reply.author.name %>" />
上面兩條語句,第一句由于使用的是單引號,用戶可以通過構造一個avatar_url中帶單引號,來截斷src屬性,后面就可以隨意加javascript代碼了。
CSRF攻擊
CSRF攻擊在node的web開發框架connect和express等中都有了解決方方案。通過在訪客的session中存放一個隨機的_csrf字段,模版引擎在生成HTML文件的時候將這個_csrf值傳遞到前端,訪客提交的任意POST請求,都必須帶上這個字段進行驗證,保證了只有當前用戶在當前頁面上可以進行修改的操作。
然而當頁面存在XSS漏洞的時候,CSRF的這種防范措施就成了浮云。惡意攻擊者完全可以通過javascript代碼,獲取到其他用戶的_csrf值,并直接模擬用戶的POST請求進行服務端數據的更改。