安全备忘单:Node.js



关于NodeJS的流行已经有很多说法。应用程序数量的增加是显而易见的-NodeJS非常易于学习,拥有大量的库以及动态发展的生态系统。

我们已基于OWASP备忘单为NodeJS开发人员准备了建议,以帮助您预测开发应用程序时的安全问题。

NodeJS应用程序的安全建议可以分为以下几类:

  • 应用程序开发过程中的安全性;
  • 服务器安全性;
  • 平台安全;


应用开发安全


避免

使用回调函数(回调)是NodeJS的最大优势之一,但是,在嵌套回调时,您很容易忘记在其中一个函数中处理错误。避免回调地狱的一种方法是使用promise。即使您使用的模块不支持使用Promise,也可以始终使用Promise.promisifyAll()。但是即使使用诺言,也要注意嵌套。为了完全避免回调地狱错误,请遵循“平坦”的承诺链。

回调地狱示例:

function func1(name, callback) {
   setTimeout(function() {
      // operations
   }, 500);
}
function func2(name, callback) {
   setTimeout(function() {
      // operations
   }, 100);
}
function func3(name, callback) {
   setTimeout(function() {
      // operations
   }, 900);
}
function func4(name, callback) {
   setTimeout(function() {
      // operations
   }, 3000);
}

func1("input1", function(err, result1){
   if(err){
      // error operations
   }
   else {
      //some operations
      func2("input2", function(err, result2){
         if(err){
            //error operations
         }
         else{
            //some operations
            func3("input3", function(err, result3){
               if(err){
                  //error operations
               }
               else{
                  // some operations
                  func4("input 4", function(err, result4){
                     if(err){
                        // error operations
                     }
                     else {
                        // some operations
                     }
                  });
               }
            });
         }
      });
   }
});

使用平链承诺的相同代码:

function func1(name, callback) {
   setTimeout(function() {
      // operations
   }, 500);
}
function func2(name, callback) {
   setTimeout(function() {
      // operations
   }, 100);
}
function func3(name, callback) {
   setTimeout(function() {
      // operations
   }, 900);
}
function func4(name, callback) {
   setTimeout(function() {
      // operations
   }, 3000);
}

func1("input1")
   .then(function (result){
      return func2("input2");
   })
   .then(function (result){
      return func3("input3");
   })
   .then(function (result){
      return func4("input4");
   })
   .catch(function (error) {
      // error operations
   });

限制请求的大小

解析请求主体可能是一项非常耗费资源的操作。如果您不限制请求的大小,攻击者将能够发送足够大的请求,这些请求可以填满所有磁盘空间或耗尽所有服务器资源,但同时,在所有情况下限制请求大小可能是不正确的,因为存在请求,例如下载文件。因此,建议为不同类型的内容设置限制。例如,使用快速框架,可以按以下方式实现:

app.use(express.urlencoded({ limit: "1kb" }));
app.use(express.json({ limit: "1kb" }));
app.use(express.multipart({ limit:"10mb" }));

应当注意,攻击者可以更改请求内容的类型和规避限制,因此,有必要检查请求的内容是否与请求标头中指定的内容类型相匹配。如果检查内容的类型会影响性能,则只能检查某些类型或大于特定大小的查询。

不要阻塞事件循环

语言的重要组成部分是事件循环,它仅允许您切换执行上下文,而无需等待操作完成。但是,有些阻塞操作的NodeJS必须等待其完成才能继续执行代码。例如,大多数同步方法正在阻塞:

const fs = require('fs');
fs.unlinkSync('/file.txt');

建议异步执行此类操作:

const fs = require('fs');
fs.unlink('/file.txt', (err) => {
    if (err) throw err;
});

同时,不要忘记异步调用之后的代码将在不等待上一个操作完成的情况下执行。

例如,在下面的代码中,文件将在读取前被删除,这可能导致竞争。

const fs = require('fs');
fs.readFile('/file.txt', (err, data) => {
  // perform actions on file content
});
fs.unlinkSync('/file.txt');

为了避免这种情况,您可以在非阻塞函数中编写所有操作:

const fs = require('fs');
fs.readFile('/file.txt', (err, data) => {
  // perform actions on file content
  fs.unlink('/file.txt', (err) => {
    if (err) throw err;
  });
});

检查输入字段

检查输入字段是任何应用程序安全性的重要组成部分。验证错误可能导致您的应用程序立即变得容易受到多种攻击:sql注入,xss,命令注入和其他攻击。为了简化表单验证,您可以使用mongo-express-sanitize验证程序包。

转义用户数据

可以保护您免受xss攻击的规则之一就是屏蔽用户数据。您可以为此使用escape-html或node-esapi库。

保留日志

此外,它将有助于调试错误,日志可用于响应事件。您可以在此处阅读有关登录需求的更多信息最流行的NodeJS日志记录软件包之一是Winston和Bunyan。下面的示例显示如何使用Winston将日志输出到控制台和文件:

var logger = new (Winston.Logger) ({
    transports: [
        new (winston.transports.Console)(),
        new (winston.transports.File)({ filename: 'application.log' })
    ],
    level: 'verbose'
});

控制事件的周期

如果您的服务器处于繁忙的网络流量中,则用户在使用服务时可能会遇到困难。这本质上是DoS攻击。在这种情况下,您可以跟踪响应时间,如果响应时间超过指定的时间,则将消息发送到503服务器太忙。toobusy-js模块可以提供帮助。

使用模块的示例:

var toobusy = require('toobusy-js');
var express = require('express');
var app = express();
app.use(function(req, res, next) {
    if (toobusy()) {
        // log if you see necessary
        res.send(503, "Server Too Busy");
    } else {
    next();
    }
});

采取预防措施以防蛮力,

再次,模块开始救援。例如,快速粗麻布或快速弹跳器。用法示例:

var bouncer = require('express-bouncer');
bouncer.whitelist.push('127.0.0.1'); // whitelist an IP address
// give a custom error message
bouncer.blocked = function (req, res, next, remaining) {
    res.send(429, "Too many requests have been made. Please wait " + remaining/1000 + " seconds.");
};
// route to protect
app.post("/login", bouncer.block, function(req, res) {
    if (LoginFailed){  }
    else {
        bouncer.reset( req );
    }
});

使用验证码是另一种常见的暴力对策。svg-captcha是帮助实现验证码的常用模块。

使用CSRF令牌

抵御CSRF攻击的最可靠方法之一是使用CSRF令牌。令牌必须以高熵生成,必须经过严格检查并与用户会话绑定。为了确保CSRF令牌的运行,可以使用csurf模块。

用法示例:

var csrf = require('csurf');
csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, function(req, res) {
    res.render('send', { csrfToken: req.csrfToken() })
})
app.post('/process', parseForm, csrfProtection, function(req, res) {
    res.send('data is being processed');
});

不要忘记将令牌添加到页面上的隐藏字段中:

<input type="hidden" name="_csrf" value="{{ csrfToken }}">

您可以在我们的文章中阅读有关CSRF令牌的更多信息

删除不必要的路由

Web应用程序不应包含用户未使用的页面,因为这会增加攻击面。因此,必须禁用所有未使用的API路由。如果使用Sails或Feathers框架,则应特别注意该问题,因为它们会自动生成API端点。

保护自己免受HPP(HTTP参数污染)的影响

默认情况下,express将请求中的所有参数添加到数组中。 OWASP建议使用hpp模块,该模块会忽略req.query和/或req.body中的所有参数值,而仅从重复的值中选择最后一个值。

var hpp = require('hpp');
app.use(hpp());

监视返回的值,

例如,用户表可以存储重要数据:密码,电子邮件地址,出生日期等。因此,仅返回必要的数据很重要。

例如:

 exports.sanitizeUser = function(user) {
  return {
    id: user.id,
    username: user.username,
    fullName: user.fullName
  };
};

使用描述符:描述

符可用于描述各种操作的属性行为:可写-是否可以更改属性的值,可枚举-是否可以在for..in循环中使用属性,可配置-是否可以覆盖属性。建议注意列出的属性,因为在定义对象的属性时,默认情况下所有这些属性都设置为true。您可以按如下所示更改属性的值:

var o = {};
Object.defineProperty(o, "a", {
    writable: true,
    enumerable: true,
    configurable: true,
    value: "A"
});

使用ACL

Acl可帮助根据角色区分数据访问。例如,添加权限如下所示:

// guest is allowed to view blogs
acl.allow('guest', 'blogs', 'view')
// allow function accepts arrays as any parameter
acl.allow('member', 'blogs', ['edit', 'view', 'delete'])

捕获uncaughtException

默认情况下,如果发生未捕获的异常,NodeJS将抛出当前堆栈跟踪并终止执行线程。但是,NodeJS允许您自定义此行为。如果发生未捕获的异常,则会引发uncaughtException事件,可以使用流程对象捕获该事件:

process.on("uncaughtException", function(err) {
    // clean up allocated resources
    // log necessary error details to log files
    process.exit(); // exit the process to avoid unknown state
});

值得记住的是,当发生uncaughtException时,您必须在完成Z进程之前清除所有分配的资源(例如,文件描述符和处理程序),以避免无法预料的错误。强烈建议如果发生uncaughtException,则程序将继续运行。

同样,在显示错误消息时,用户不应泄露详细的错误信息,例如堆栈跟踪。

服务器安全性


在使用cookie时,设置标头的标志

有几种标志可以帮助防止xss和csrf等攻击:httpOnly,它可以防止通过javascript访问cookie;安全-仅允许通过HTTPS和SameSite发送cookie,这决定了将cookie传输到第三方资源的能力。

用法示例:

var session = require('express-session');
app.use(session({
    secret: 'your-secret-key',
    key: 'cookieName',
    cookie: { secure: true, httpOnly: true, path: '/user', sameSite: true}
}));

设置HTTP标头以提高安全性

以下是标头和如何连接它们的示例,以帮助您保护自己免受多种常见攻击。标头使用头盔模块设置

•严格传输安全性:HTTP严格传输安全性(HSTS)告诉浏览器只能通过HTTPS访问该应用程序

app.use(helmet.hsts()); // default configuration
app.use(helmet.hsts("<max-age>", "<includeSubdomains>")); // custom configuration

•X-Frame-Options:确定页面是否可以在框架,iframe,嵌入或对象中使用

app.use(hemlet.xframe()); // default behavior (DENY)
helmet.xframe('sameorigin'); // SAMEORIGIN
helmet.xframe('allow-from', 'http://alloweduri.com'); //ALLOW-FROM uri

•X-XSS-Protection:如果浏览器检测到反射的XSS攻击,则允许浏览器停止加载页面。

var xssFilter = require('x-xss-protection');
app.use(xssFilter());

•X-Content-Type-Options:用于防止使用MIME类型的攻击

app.use(helmet.noSniff());

•内容安全策略:防止诸如XSS和数据注入攻击之类的攻击

const csp = require('helmet-csp')
app.use(csp({
   directives: {
       defaultSrc: ["'self'"],  // default value for all directives that are absent
       scriptSrc: ["'self'"],   // helps prevent XSS attacks
       frameAncestors: ["'none'"],  // helps prevent Clickjacking attacks
       imgSrc: ["'self'", "'http://imgexample.com'"],
       styleSrc: ["'none'"]
    }
}))

•Cache-Control和Pragma:对于管理缓存,尤其是此标头对于包含敏感数据的页面很有用。但是,请记住,禁用所有页面上的缓存会影响性能。

app.use(helmet.noCache());

•X-Download-Options:标头可防止Inter Explorer执行下载的文件

app.use(helmet.ieNoOpen());

•Expect-CT:证书透明性-一种为解决SSL证书基础结构中的某些问题而创建的机制,此标头告知浏览器CT日志中需要其他证书验证

var expectCt = require('expect-ct');
app.use(expectCt({ maxAge: 123 }));
app.use(expectCt({ enforce: true, maxAge: 123 }));
app.use(expectCt({ enforce: true, maxAge: 123, reportUri: 'http://example.com'}));

•X-Powered-By:可选标头,用于指示服务器上使用的技术。您可以按以下方式隐藏此标题:

app.use(helmet.hidePoweredBy());

此外,您可以更改该值以隐藏有关所使用技术的真实信息:

app.use(helmet.hidePoweredBy({ setTo: 'PHP 4.2.0' }));

平台安全


更新软件包

应用程序的安全性取决于您使用的软件包的安全性,因此使用软件包的最新版本非常重要。为确保您使用的软件包不包含已知漏洞,可以使用特殊的OWASP列表。您还可以使用检查程序包中是否存在已知漏洞Retire.js的库。

请勿使用不安全的功能。

建议尽可能删除一些功能。在这些函数中有eval(),它执行一个作为参数的字符串。与用户输入结合使用时,使用此函数可能会导致远程执行代码漏洞,因为出于类似的原因,使用child_process.exec也是不安全的,因为该函数会将接收到的参数传递给bin / sh。

此外,应谨慎使用许多模块。例如,用于文件的fs模块。如果以某种方式将生成的用户输入传递给函数,则您的应用程序可能会变得容易受到本地文件和目录遍历的影响。

vm模块提供用于在V8虚拟机上编译和运行代码的API,仅应在沙箱中使用。

在这里,您可以熟悉可能会使您的应用程序不安全的其他函数;

使用正则表达式时要小心;

可以编写正则表达式,以使表达式可以成指数增长,从而导致拒绝服务。这种攻击称为ReDoS。有几种工具可以检查正则表达式是否安全,其中之一就是vuln-regex-detector。

定期运行lint

在开发过程中,很难牢记所有安全建议,而在团队开发方面,要使所有团队成员都遵守规则并不容易。为此,有一些用于静态安全分析的工具。此类工具无需执行代码即可在其中寻找漏洞。此外,lint允许您添加自定义规则,以在代码中查找可能易受攻击的位置。最常用的短绒是ESLint和JSHint。

使用严格模式

Javascript有许多不安全和过时的功能,不应使用。为了排除使用这些功能的可能性,还提供了严格模式。

遵守一般安全原则

所描述的建议集中在NodeJS上,但不要忘记无论使用什么平台,都必须遵守的一般安全性原则。

All Articles