前言

早年在浏览器大战期间,有远见的Chrome认为要运行现代Web应用,浏览器必须有一个性能非常强劲的JavaScript引擎,于是Google自己开发了一个高性能的开源的JavaScript引擎,名字叫V8。在2009年,Ryan正式推出了基于JavaScript语言和V8引擎的开源Web服务器项目,命名为Node.js。

从此JavaScript也可以在后端服务器开发,由于已经有很多的JavaScript开发人员,所以Node很快流行起来了。Node.js可以安装在linux、mac/windows平台上, npm其实是Node.js的包管理工具(package manager),它可以让开发人员方便的安装、卸载js模块,并且自动的解决依赖关系。Node.js可以自己作为服务器,监听端口,处理客户端的请求,由于node.js本身是单线程的,为了缓解可能的dos攻击,可以采用nginx第三方服务器部署成负载均衡的多实例方式。目前node.js社区已经诞生了很多优秀的web框架,比如 Express是第一代最流行的web框架,koa是Express的下一代基于Node.js的web框架。本文主要从安全的角度来分析node.js的特性。

示例代码
本文的事例代码采用了express框架,共分为以下几个部分:有3个文件和1个文件夹。

file.html //稍后用到的上传页面
index.js //主要的文件,包含了xss、ssrf、文件长传、sql等例子
package.json //记录了用到的js模块等信息
node_modules //是一个文件夹,js模块所在的目录


Package.json
{
“dependencies”: {
“body-parser”: “^1.18.2″,
“cookie-parser”: “^1.4.3″,
“express”: “^4.16.2″,
“multer”: “^1.3.0″
}
}


Index.js

var express = require(‘express’);
var child_process = require(‘child_process’);
var helmet = require(‘helmet’);
var needle = require(‘needle’);
var app = express();
var fs = require(“fs”);
var multer = require(‘multer’);
var bodyParser = require(‘body-parser’);
app.use(helmet());
app.use(multer({ dest: ‘/tmp/’}).array(‘image’));
app.use(express.static(‘public’));
app.get(‘/’, function (req, res) {
res.send(‘Hello World’);
})

app.get(‘/file.html’, function (req, res) {
res.sendFile( __dirname + “/” + “file.html” );
})

app.get(‘/eval’,function(req,res){
res.send(eval(“req.query.q”));
})

app.post(‘/file’,function(req,res){
console.log(req.files[0]); //
var des_file = __dirname + “/” + req.files[0].originalname;
fs.readFile( req.files[0].path, function (err, data) {
fs.writeFile(des_file, data, function (err) {
if( err ){
console.log( err );
}else{
response = {
message:’File uploaded successfully’,
filename:req.files[0].originalname
};
}
console.log( response );
res.end( JSON.stringify( response ) );
});
});
})

app.get(‘/xss’,function(req,res) {
res.send(req.query.q);
})

app.get(‘/ssrf’,function(req,res){
var url=req.query['url'];
needle.get(url)
console.log(‘new request:’+url);
})

app.get(‘/rce’, function (req, res) {
child_process.exec(req.query.q);
console.log(res);
})

var server = app.listen(50000, function () {
var host = server.address().address
var port = server.address().port
console.log(“应用实例,访问地址为 http://www.jshaman.com/“, host, port)
})



设置安全的HTTP头
在Node.js中可以通过强制设置一些安全的HTTP头来加强网站的安全系数,比如以下:

Strict-Transport-Security //强制使用安全连接(SSL/TLS之上的HTTPS)来连接到服务器。
X-Frame-Options //提供对于点击劫持的保护。
X-XSS-Protection //开启大多现代浏览器内建的对于跨站脚本攻击(XSS)的过滤功能。
X-Content-Type-Options // 防止浏览器使用MIME-sniffing 来确定响应的类型,转而使用明确的content-type来确定。
Content-Security-Policy // 防止受到跨站脚本攻击以及其他跨站注入攻击。

目前Helnet第三方模块已经帮开发人员设置好了,直接将它引入到我们的系统就可以了。

var express = require( ‘express’);
var helmet = require( ‘helmet’);
var app = express();
app.use(helmet());




代码执行
熟悉php渗透的朋友都领教过eval()在木马文件里的妙用。js也有eval()函数,由于强大的功能甚至被誉为“魔鬼”。它在js中的功能跟php是差不多的,即动态的执行代码。假如客户端的输入直接丢到eval函数里面执行,轻则产生各种xss弹框,但通常攻击者都会用它调用关键函数来执行系统命令。


app.get(‘/eval’,function(req,res){
res.send(eval(“req.query.q”));
})



除了eval函数能动态执行代码,setInteval、setTimeout、 new Function等函数也有相同的功能,因此在使用的使用要小心谨慎。


命令执行
在Node.js中child_process.exec命令调用的是/bin/sh,因此它是一个bash解释器, 可以执行系统命令,若其直接接受外部参数 则可能造成RCE漏洞。


app.get(‘/rce’, function (req, res) {
child_process.exec(req.query.q);
console.log(res);
})


成功执行了运行计算器的命令:



xss
Node.js不像java有很强大的过滤器,过滤用户的有害输入、缓解xss十分方便。但是可以通过设置HTTP头中加入X-XSS-Protection,来开起浏览器内建的对于跨站脚本攻击(XSS)的过滤功能。由于本身没有xss防护机制 ,若是未经过滤直接显示外部的输入则导致XSS。


app.get(‘/xss’,function(req,res) {
res.send(req.query.q);
})



程序直接将用户的输入显示到前端页面:



sql注入

Node.js的网站注入漏洞很少。Node.js通常与mysql/mongodb搭配使用,因为sql注入的漏洞危害很高并且存在多年了,一些新出现的语言如openresty+lua/node.js等天生会规避掉这种安全问题。它们通常都采用了占位符或者叫参数化查询来与数据库交互。node.js 原生的与数据库交互代码如下:

var mysql = require (‘mysql’ ) ;
var connection = mysql .createConnection(
{ host : ‘ localhost’,
user : ‘root ’,
password : ‘root ’,
port: ’3306 ’,
database: ‘admin ‘, }) ;
connection.connect( );
var sql = ’ select * from admin where id =?’;
Var param=[1];
connection.query( sql,param);
connection.end( );

Node.js现在已经有了orm框架(比如Sequelize),因此注入漏洞就跟少了。但是如果程序员写代码时不小心用了字符串拼接,还是会造成sql注入的。如下:


select * from admin where id=$id



ssrf

ssrf漏洞在存在于大多数的编程语言中,node.js也不例外,只要web系统接收了外界输入的URL,并且通过服务端程序直接调用就会造成相应的漏洞。

app.get(‘/ssrf’,function(req,res){
var url=req.query['url'];
needle.get(url)
console.log(‘new request:’+url);
})


文件上传

Node.js的网站由于特有的路由规则,它的的上传问题虽然不像php、jsp、asp等脚本语言,若攻击者上传若未经过滤的脚本,便可轻松的拿到shel。但是代码中若存在路径跳转漏洞,攻击者可以直接将shell脚本木马上传到/etc/rc.d等启动项下面,或者是直接上传相应的index.js文件覆盖到第三方模块express等目录下,通过精心构造的js文件也能实现命令执行的目的。

app.post(‘/file’,function(req,res){
console.log(req.files[0]); // 上传的文件信息
var des_file = __dirname + “/” + req.files[0].originalname;
fs.readFile( req.files[0].path, function (err, data) {
fs.writeFile(des_file, data, function (err) {
if( err ){
console.log( err );
}else{
response = {
message:’File uploaded successfully’,
filename:req.files[0].originalname
};
}
console.log( response );
res.end( JSON.stringify( response ) );
});
});
})


NPM

任何人都可以创建模块发布到npm上,供别人调用,虽然这为开发者带来了一定的便利性,但必然隐藏着安全隐患,假如一不小心使用了不安全的第三方模块后果可想而知了,比如前段时间闹得沸沸扬扬的node-serialize模块所引起的远程代码执行漏洞(cve-2017-5914)。现在有一款NSP 工具可以帮助检查第三方模块现有漏洞。
npm i nsp –g //安装nsp
nsp check 要检查的package.json //检查是否有漏洞

NodeJS代码混淆加密,就用JShamanhttp://www.jshaman.com/)。

总结:

Node.js最为人诟病的就是单线程,大部分任务都在一个线程中完成,单线程虽然省去了频繁的切换线程,也不存在资源互占的问题,但面对cpu密集型的任务就力不从心了。Node.js具有异步机制,可以把一些耗时算法丢人eventloop等待下个事件循环再做,但任务量大了相比java等多线程机制的语言还是容易造成服务器崩溃。即使有这个缺点,但并不影响Node.js的流行。仗者JS在前端的绝对的统治地位,我们有理由相信Node.js会越来越流行,研究Node.js的同行注定也会越来越多。