跳到内容

Node.js 安全速查表

简介

本速查表列出了开发者可以采取的措施,以开发安全的 Node.js 应用程序。每个条目都提供了针对 Node.js 环境的简要解释和解决方案。

背景

Node.js 应用程序的数量正在增加,它们与其他框架和编程语言并没有什么不同。Node.js 应用程序容易受到各种 Web 应用程序漏洞的影响。

目标

本速查表旨在提供在开发 Node.js 应用程序时应遵循的最佳实践列表。

建议

有几项建议可以增强 Node.js 应用程序的安全性。这些建议分为:

  • 应用程序安全
  • 错误与异常处理
  • 服务器安全
  • 平台安全

应用程序安全

使用扁平的 Promise 链

异步回调函数是 Node.js 最强大的特性之一。然而,回调函数中日益增加的嵌套层级可能会成为一个问题。任何多阶段过程都可能嵌套 10 层或更深。这个问题被称为“厄运金字塔”或“回调地狱”。在这种代码中,错误和结果会丢失在回调中。Promises 是一种很好的方式来编写异步代码,而不会陷入嵌套金字塔。Promises 提供自上而下的执行,同时通过将错误和结果传递给下一个 .then 函数来实现异步。

Promises 的另一个优点是 Promises 处理错误的方式。如果在 Promise 类中发生错误,它会跳过 .then 函数并调用它找到的第一个 .catch 函数。这样,Promises 为捕获和处理错误提供了更高的保证。原则上,你可以让所有异步代码(除了 EventEmitter)都返回 Promise。需要注意的是,Promise 调用也可能形成金字塔。为了完全避免“回调地狱”,应使用扁平的 Promise 链。如果你使用的模块不支持 Promises,可以通过使用 Promise.promisifyAll() 函数将基础对象转换为 Promise。

以下代码片段是“回调地狱”的一个例子

function func1(name, callback) {
  // operations that takes a bit of time and then calls the callback
}
function func2(name, callback) {
  // operations that takes a bit of time and then calls the callback
}
function func3(name, callback) {
  // operations that takes a bit of time and then calls the callback
}
function func4(name, callback) {
  // operations that takes a bit of time and then calls the callback
}

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
                     }
                  });
               }
            });
         }
      });
   }
});

上述代码可以使用扁平的 Promise 链安全地改写如下

function func1(name) {
  // operations that takes a bit of time and then resolves the promise
}
function func2(name) {
  // operations that takes a bit of time and then resolves the promise
}
function func3(name) {
  // operations that takes a bit of time and then resolves the promise
}
function func4(name) {
  // operations that takes a bit of time and then resolves the promise
}

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
   });

以及使用 async/await

async function func1(name) {
  // operations that takes a bit of time and then resolves the promise
}
async function func2(name) {
  // operations that takes a bit of time and then resolves the promise
}
async function func3(name) {
  // operations that takes a bit of time and then resolves the promise
}
async function func4(name) {
  // operations that takes a bit of time and then resolves the promise
}

(async() => {
  try {
    let res1 = await func1("input1");
    let res2 = await func2("input2");
    let res3 = await func3("input2");
    let res4 = await func4("input2");
  } catch(err) {
    // error operations
  }
})();

设置请求大小限制

缓冲和解析请求体可能是一项资源密集型任务。如果对请求大小没有限制,攻击者可以发送带有大请求体的请求,从而耗尽服务器内存和/或填满磁盘空间。你可以使用 raw-body 来限制所有请求的请求体大小。

const contentType = require('content-type')
const express = require('express')
const getRawBody = require('raw-body')

const app = express()

app.use(function (req, res, next) {
  if (!['POST', 'PUT', 'DELETE'].includes(req.method)) {
    next()
    return
  }

  getRawBody(req, {
    length: req.headers['content-length'],
    limit: '1kb',
    encoding: contentType.parse(req).parameters.charset
  }, function (err, string) {
    if (err) return next(err)
    req.text = string
    next()
  })
})

然而,为所有请求设置固定的请求大小限制可能不是正确的做法,因为某些请求的请求体中可能包含大型有效负载,例如上传文件时。此外,JSON 类型的输入比 multipart 输入更危险,因为解析 JSON 是一项阻塞操作。因此,你应该为不同的内容类型设置请求大小限制。你可以通过 express 中间件非常容易地实现这一点,如下所示:

app.use(express.urlencoded({ extended: true, limit: "1kb" }));
app.use(express.json({ limit: "1kb" }));

应该注意的是,攻击者可以更改请求的 Content-Type 标头并绕过请求大小限制。因此,在处理请求之前,应该根据请求标头中声明的内容类型验证请求中包含的数据。如果每个请求的内容类型验证严重影响性能,你可以只验证特定的内容类型或大于预定大小的请求。

不要阻塞事件循环

Node.js 与使用线程的常见应用程序平台大不相同。Node.js 采用单线程事件驱动架构。通过这种架构,吞吐量变得很高,编程模型也变得更简单。Node.js 是围绕非阻塞 I/O 事件循环实现的。有了这个事件循环,就没有 I/O 等待或上下文切换。事件循环查找事件并将其分派给处理函数。正因为如此,当执行 CPU 密集型 JavaScript 操作时,事件循环会等待它们完成。这就是为什么这些操作被称为“阻塞”。为了克服这个问题,Node.js 允许将回调分配给 I/O 阻塞事件。这样,主应用程序就不会被阻塞,并且回调会异步运行。因此,作为一般原则,所有阻塞操作都应该异步进行,以便事件循环不被阻塞。

即使你异步执行阻塞操作,你的应用程序可能仍无法按预期提供服务。如果回调之外的代码依赖于回调内的代码首先运行,就会发生这种情况。例如,考虑以下代码:

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

在上面的示例中,unlinkSync 函数可能在回调之前运行,这将在文件内容所需的操作完成之前删除文件。这种竞态条件也会影响应用程序的安全性。一个例子是,在回调中执行身份验证,然后同步运行已验证的操作。为了消除这种竞态条件,你可以将所有相互依赖的操作写入单个非阻塞函数中。通过这样做,你可以保证所有操作都以正确的顺序执行。例如,上面的代码示例可以通过非阻塞方式编写如下:

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 注入、跨站脚本、命令注入、本地/远程文件包含、拒绝服务、目录遍历、LDAP 注入和许多其他注入攻击。为了避免这些攻击,应用程序的输入应首先进行净化。最佳的输入验证技术是使用可接受输入的列表。然而,如果这不可能,则应首先根据预期的输入方案检查输入,并对危险输入进行转义。为了简化 Node.js 应用程序中的输入验证,有一些模块,例如 validatorexpress-mongo-sanitize。有关输入验证的详细信息,请参阅 输入验证速查表

JavaScript 是一种动态语言,根据框架解析 URL 的方式,应用程序代码看到的数据可以有多种形式。以下是在 express.js 中解析查询字符串后的一些示例:

URL 代码中 request.query.foo 的内容
?foo=bar 'bar' (字符串)
?foo=bar&foo=baz ['bar', 'baz'] (字符串数组)
?foo[]=bar ['bar'] (字符串数组)
?foo[]=bar&foo[]=baz ['bar', 'baz'] (字符串数组)
?foo[bar]=baz { bar : 'baz' } (带键的对象)
?foo[]baz=bar ['bar'] (字符串数组 - 后缀丢失)
?foo[][baz]=bar [ { baz: 'bar' } ] (对象数组)
?foo[bar][baz]=bar { foo: { bar: { baz: 'bar' } } } (对象树)
?foo[10]=bar&foo[9]=baz [ 'baz', 'bar' ] (字符串数组 - 注意顺序)
?foo[toString]=bar {} (调用 toString() 将失败的对象)

执行输出转义

除了输入验证,您还应该转义通过应用程序向用户显示的所有 HTML 和 JavaScript 内容,以防止跨站脚本 (XSS) 攻击。您可以使用 escape-htmlnode-esapi 库来执行输出转义。

执行应用程序活动日志记录

记录应用程序活动是一种值得鼓励的良好实践。它使得在应用程序运行时遇到的任何错误更容易调试。它也对安全问题很有用,因为可以在事件响应期间使用它。此外,这些日志可以用于馈送入侵检测/防御系统(IDS/IPS)。在 Node.js 中,有诸如 WinstonBunyanPino 等模块来执行应用程序活动日志记录。这些模块支持日志流式传输和查询,并提供了一种处理未捕获异常的方式。

使用以下代码,你可以在控制台和所需的日志文件中记录应用程序活动:

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

你可以提供不同的传输方式,以便将错误保存到单独的日志文件,将一般应用程序日志保存到另一个日志文件。有关安全日志记录的更多信息,请参见 日志记录速查表

监控事件循环

当你的应用程序服务器面临大量网络流量时,它可能无法为用户提供服务。这本质上是一种拒绝服务 (DoS) 攻击。toobusy-js 模块允许你监控事件循环。它跟踪响应时间,当响应时间超过某个阈值时,该模块可以指示你的服务器过于繁忙。在这种情况下,你可以停止处理传入请求并向它们发送 503 Server Too Busy 消息,以使你的应用程序保持响应。这里展示了 toobusy-js 模块的示例用法:

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

采取防范暴力破解的措施

暴力破解是对所有 Web 应用程序的常见威胁。攻击者可以使用暴力破解作为密码猜测攻击来获取帐户密码。因此,应用程序开发人员应采取预防措施,尤其是在登录页面中防范暴力破解攻击。Node.js 为此提供了多个可用模块。Express-bouncerexpress-bruterate-limiter 仅是其中一些示例。根据你的需求和要求,你应该选择其中一个或多个模块并相应使用。Express-bouncerexpress-brute 模块的工作方式类似。它们增加每次失败请求的延迟,并且可以针对特定路由进行配置。这些模块可以按如下方式使用:

const bouncer = require('express-bouncer');
bouncer.whitelist.push('127.0.0.1'); // allow an IP address
// give a custom error message
bouncer.blocked = function (req, res, next, remaining) {
    res.status(429).send("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 );
    }
});
const ExpressBrute = require('express-brute');

const store = new ExpressBrute.MemoryStore(); // stores state locally, don't use this in production
const bruteforce = new ExpressBrute(store);

app.post('/auth',
    bruteforce.prevent, // error 429 if we hit this route too often
    function (req, res, next) {
        res.send('Success!');
    }
);

除了 express-bouncerexpress-brute 之外,rate-limiter 模块也有助于防止暴力破解攻击。它允许指定特定 IP 地址在指定时间段内可以发出多少请求。

const limiter = new RateLimiter();
limiter.addLimit('/login', 'GET', 5, 500); // login page can be requested 5 times at max within 500 seconds

CAPTCHA 的使用也是一种常用于对抗暴力破解的机制。Node.js 有专门开发的 CAPTCHA 模块。Node.js 应用程序中常用的模块是 svg-captcha。它可以按如下方式使用:

const svgCaptcha = require('svg-captcha');
app.get('/captcha', function (req, res) {
    const captcha = svgCaptcha.create();
    req.session.captcha = captcha.text;
    res.type('svg');
    res.status(200).send(captcha.data);
});

账户锁定是阻止攻击者接近您的有效用户的推荐解决方案。许多模块(例如 mongoose)都可以实现账户锁定。您可以参考这篇博客文章,了解如何在 mongoose 中实现账户锁定。

使用 Anti-CSRF 令牌

跨站请求伪造 (CSRF) 旨在代表经过身份验证的用户执行授权操作,而用户对此操作毫不知情。CSRF 攻击通常针对改变状态的请求执行,例如更改密码、添加用户或下订单。Csurf 是一个曾用于缓解 CSRF 攻击的 express 中间件。但最近在该包中发现了一个安全漏洞。该包的开发团队尚未修复发现的漏洞,并且已将该包标记为弃用,建议使用任何其他 CSRF 保护包。

有关跨站请求伪造 (CSRF) 攻击和预防方法的详细信息,请参阅 跨站请求伪造预防

移除不必要的路由

Web 应用程序不应包含任何用户未使用的页面,因为它可能会增加应用程序的攻击面。因此,在 Node.js 应用程序中应禁用所有未使用的 API 路由。这种情况尤其发生在 SailsFeathers 等框架中,因为它们会自动生成 REST API 端点。例如,在 Sails 中,如果 URL 与自定义路由不匹配,它可能会匹配其中一个自动路由并仍生成响应。这种情况可能导致从信息泄露到任意命令执行的后果。因此,在使用此类框架和模块之前,了解它们自动生成的路由并移除或禁用这些路由非常重要。

防止 HTTP 参数污染

HTTP 参数污染 (HPP) 是一种攻击,攻击者发送多个同名的 HTTP 参数,导致您的应用程序不可预测地解释它们。当发送多个参数值时,Express 会将它们填充到一个数组中。为了解决这个问题,您可以使用 hpp 模块。使用时,此模块将忽略 req.query 和/或 req.body 中为参数提交的所有值,而只选择最后提交的参数值。您可以按如下方式使用它:

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

仅返回必要信息

有关应用程序用户的信息是应用程序中最关键的信息之一。用户表通常包括 ID、用户名、全名、电子邮件地址、出生日期、密码,在某些情况下还包括社会安全号码等字段。因此,在查询和使用用户对象时,您只需要返回所需的字段,因为它可能容易受到个人信息泄露的影响。这对于存储在数据库中的其他对象也是正确的。如果您只需要对象的某个特定字段,则应仅返回所需的特定字段。例如,每当您需要获取用户的信息时,您都可以使用如下所示的函数。通过这样做,您可以只返回特定操作所需的字段。换句话说,如果您只需要列出可用用户的姓名,您就不会在他们的全名之外返回他们的电子邮件地址或信用卡号码。

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

使用对象属性描述符

对象属性包含三个隐藏属性:writable(如果为 false,则属性值无法更改)、enumerable(如果为 false,则属性不能用于 for 循环)和 configurable(如果为 false,则属性不能删除)。通过赋值定义对象属性时,这三个隐藏属性默认设置为 true。这些属性可以按如下方式设置:

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

除了这些,还有一些针对对象属性的特殊函数。Object.preventExtensions() 可以防止向对象添加新属性。

使用访问控制列表

授权防止用户在预期权限之外进行操作。为此,应根据最小权限原则确定用户及其角色。每个用户角色应仅有权访问他们必须使用的资源。对于您的 Node.js 应用程序,您可以使用 acl 模块来提供 ACL(访问控制列表)实现。使用此模块,您可以创建角色并将用户分配给这些角色。

错误与异常处理

处理未捕获异常

Node.js 对未捕获异常的行为是打印当前堆栈跟踪,然后终止线程。然而,Node.js 允许自定义此行为。它提供了一个名为 process 的全局对象,该对象可用于所有 Node.js 应用程序。它是一个 EventEmitter 对象,在发生未捕获异常时,会发出 uncaughtException 事件并将其提升到主事件循环。为了提供未捕获异常的自定义行为,你可以绑定到此事件。但是,在此类未捕获异常后恢复应用程序可能会导致进一步的问题。因此,如果你不想错过任何未捕获异常,你应该绑定到 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
});

使用 EventEmitter 时监听错误

使用 EventEmitter 时,错误可能发生在事件链中的任何位置。通常,如果 EventEmitter 对象中发生错误,则会调用一个以 Error 对象作为参数的错误事件。但是,如果该错误事件没有附加的监听器,则作为参数发送的 Error 对象将被抛出并成为未捕获异常。简而言之,如果您未能正确处理 EventEmitter 对象中的错误,这些未处理的错误可能会导致您的应用程序崩溃。因此,在使用 EventEmitter 对象时,您应该始终监听错误事件。

const events = require('events');
const myEventEmitter = function(){
    events.EventEmitter.call(this);
}
require('util').inherits(myEventEmitter, events.EventEmitter);
myEventEmitter.prototype.someFunction = function(param1, param2) {
    //in case of an error
    this.emit('error', err);
}
const emitter = new myEventEmitter();
emitter.on('error', function(err){
    //Perform necessary error handling here
});

处理异步调用中的错误

异步回调中发生的错误很容易被忽略。因此,作为一般原则,异步调用的第一个参数应该是 Error 对象。此外,express 路由本身会处理错误,但应始终记住,express 路由内进行的异步调用中发生的错误不会被处理,除非将 Error 对象作为第一个参数发送。

这些回调中的错误可以尽可能多地传播。错误传播到的每个回调都可以忽略、处理或传播错误。

服务器安全

通常,Web 应用程序中使用 Cookie 来发送会话信息。然而,HTTP Cookie 的不当使用可能导致应用程序出现多种会话管理漏洞。可以为每个 Cookie 设置一些标志来防止此类攻击。httpOnlySecureSameSite 标志对于会话 Cookie 非常重要。httpOnly 标志可防止客户端 JavaScript 访问 Cookie。这是 XSS 攻击的有效对策。Secure 标志允许仅在通信通过 HTTPS 进行时才发送 Cookie。SameSite 标志可以防止 Cookie 在跨站请求中发送,这有助于防御跨站请求伪造 (CSRF) 攻击。除此之外,还有其他标志,如 domain、path 和 expires。鼓励适当地设置这些标志,但它们主要与 Cookie 范围而非 Cookie 安全性相关。以下示例展示了这些标志的用法:

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

使用适当的安全标头

有几个 HTTP 安全标头 可以帮助您防止一些常见的攻击向量。helmet 包可以帮助设置这些标头。

const express = require("express");
const helmet = require("helmet");

const app = express();

app.use(helmet()); // Add various HTTP headers

顶层 helmet 函数是 14 个小型中间件的包装器。下面列出了 helmet 中间件涵盖的 HTTP 安全标头:

app.use(helmet.hsts()); // default configuration
app.use(
  helmet.hsts({
    maxAge: 123456,
    includeSubDomains: false,
  })
); // custom configuration
  • X-Frame-Options: 确定页面是否可以通过 <frame><iframe> 元素加载。允许页面被框架加载可能导致 点击劫持 攻击。
app.use(helmet.frameguard()); // default behavior (SAMEORIGIN)
  • X-XSS-Protection: 当检测到反射型跨站脚本 (XSS) 攻击时,阻止页面加载。此标头已被现代浏览器弃用,其使用可能会在客户端引入额外的安全问题。因此,建议将标头设置为 X-XSS-Protection: 0 以禁用 XSS Auditor,并且不允许它采用浏览器处理响应的默认行为。
app.use(helmet.xssFilter()); // sets "X-XSS-Protection: 0"

对于现代浏览器,建议实现强大的 Content-Security-Policy 策略,详情见下一节。

  • Content-Security-Policy: 内容安全策略的开发旨在降低 跨站脚本 (XSS)点击劫持 等攻击的风险。它允许来自您决定的列表中的内容。它有几个指令,每个指令都禁止加载特定类型的内容。您可以参考 内容安全策略速查表,了解每个指令的详细解释以及如何使用它。您可以在应用程序中按如下方式实现这些设置:
app.use(
  helmet.contentSecurityPolicy({
    // the following directives will be merged into the default helmet CSP policy
    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'"]
    }
  })
);

由于此中间件执行的验证非常少,因此建议改为依赖像 CSP Evaluator 这样的 CSP 检查器。

  • X-Content-Type-Options: 即使服务器在响应中设置了有效的 Content-Type 标头,浏览器也可能尝试嗅探请求资源的 MIME 类型。此标头是一种阻止此行为并告知浏览器不要更改 Content-Type 标头中指定的 MIME 类型的方法。它可以按以下方式配置:
app.use(helmet.noSniff());
  • Cache-ControlPragma: Cache-Control 标头可用于防止浏览器缓存给定响应。这应针对包含用户或应用程序敏感信息的页面进行。然而,对于不包含敏感信息的页面禁用缓存可能会严重影响应用程序的性能。因此,缓存应仅对返回敏感信息的页面禁用。可以使用 nocache 包轻松设置适当的缓存控制和标头:
const nocache = require("nocache");

app.use(nocache());

上述代码相应地设置了 Cache-Control、Surrogate-Control、Pragma 和 Expires 标头。

  • X-Download-Options: 此标头可防止 Internet Explorer 在站点上下文中执行下载的文件。这是通过 noopen 指令实现的。您可以使用以下代码片段实现:
app.use(helmet.ieNoOpen());
  • X-Powered-By: X-Powered-By 标头用于告知服务器端使用了哪些技术。这是一个不必要的标头,会导致信息泄露,因此应从您的应用程序中删除。为此,您可以按如下方式使用 hidePoweredBy
app.use(helmet.hidePoweredBy());

此外,您还可以通过此标头谎报所使用的技术。例如,即使您的应用程序不使用 PHP,您也可以将 X-Powered-By 标头设置为看起来像是使用了 PHP。

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

平台安全

保持您的包最新

应用程序的安全性直接取决于您在应用程序中使用的第三方包的安全性。因此,保持您的包最新非常重要。值得注意的是,使用具有已知漏洞的组件 仍然在 OWASP Top 10 中。您可以使用 OWASP Dependency-Check 来检查项目中使用的任何包是否存在已知漏洞。此外,您还可以使用 Retire.js 来检查具有已知漏洞的 JavaScript 库。

从版本 6 开始,npm 引入了 audit 命令,它会警告存在漏洞的包。

npm audit

npm 还引入了一种简单的方法来升级受影响的包:

npm audit fix

您可以使用其他几种工具来检查您的依赖项。更全面的列表可以在 漏洞依赖管理速查表 中找到。

不要使用危险函数

有些 JavaScript 函数很危险,应仅在必要或不可避免的情况下使用。第一个例子是 eval() 函数。此函数接受一个字符串参数并将其作为任何其他 JavaScript 源代码执行。结合用户输入,这种行为固有地导致远程代码执行漏洞。同样,调用 child_process.exec 也非常危险。此函数充当 bash 解释器,并将其参数发送到 /bin/sh。通过向此函数注入输入,攻击者可以在服务器上执行任意命令。

除了这些函数,有些模块在使用时需要特别小心。例如,fs 模块处理文件系统操作。但是,如果未正确净化用户输入并将其提供给此模块,您的应用程序可能会容易受到文件包含和目录遍历漏洞的影响。类似地,vm 模块提供了在 V8 虚拟机上下文中编译和运行代码的 API。由于它本质上可以执行危险操作,因此应在沙箱中使用。

说这些函数和模块根本不应该使用是不公平的,但是,它们应该谨慎使用,尤其是在与用户输入一起使用时。此外,还有 一些其他函数 可能会使您的应用程序易受攻击。

避免使用“邪恶”的正则表达式

正则表达式拒绝服务 (ReDoS) 是一种拒绝服务攻击,它利用了大多数正则表达式实现在极端情况下可能导致其工作速度非常缓慢(与输入大小呈指数关系)的事实。攻击者可以使使用正则表达式的程序进入这些极端情况,然后长时间挂起。

正则表达式拒绝服务 (ReDoS) 是一种使用正则表达式的拒绝服务攻击。某些正则表达式 (Regex) 实现会导致极端情况,使应用程序运行非常缓慢。攻击者可以使用此类正则表达式实现,使应用程序陷入这些极端情况并长时间挂起。如果应用程序可能因精心制作的输入而卡住,则此类正则表达式被称为“邪恶”。通常,这些正则表达式通过重复分组和重叠交替来利用。例如,以下正则表达式 ^(([a-z])+.)+[A-Z]([a-z])+$ 可用于指定 Java 类名。然而,一个非常长的字符串(aaaa...aaaaAaaaaa...aaaa)也可以匹配此正则表达式。有一些工具可以检查正则表达式是否有可能导致拒绝服务。一个例子是 vuln-regex-detector

运行安全检查工具

在开发代码时,牢记所有安全提示可能非常困难。此外,让所有团队成员遵守这些规则几乎是不可能的。这就是为什么存在静态分析安全测试 (SAST) 工具。这些工具不执行您的代码,但它们只是寻找可能包含安全风险的模式。由于 JavaScript 是一种动态且弱类型的语言,因此 linting 工具在软件开发生命周期中至关重要。linting 规则应定期审查,并且应审计发现。这些工具的另一个优点是您可以为可能认为是危险的模式添加自定义规则。ESLintJSHint 是常用的 JavaScript linting SAST 工具。

使用严格模式

JavaScript 有许多不安全且危险的遗留特性不应使用。为了移除这些特性,ES5 为开发者引入了严格模式。在此模式下,以前静默的错误会被抛出。它还有助于 JavaScript 引擎执行优化。在严格模式下,以前接受的不良语法会导致实际错误。由于这些改进,您应该始终在应用程序中使用严格模式。要启用严格模式,您只需在代码顶部写入 "use strict";

以下代码将在控制台上生成 ReferenceError: Can't find variable: y,除非使用严格模式,否则不会显示。

"use strict";

func();
function func() {
  y = 3.14;   // This will cause an error (y is not defined)
}

遵循通用的应用程序安全原则

本列表主要侧重于 Node.js 应用程序中常见的问题,并提供建议和示例。除此之外,还有一些通用的 安全设计原则,适用于所有 Web 应用程序,无论应用程序服务器使用何种技术。在开发应用程序时,您也应该牢记这些原则。您可以随时参考 OWASP 速查表系列,了解更多关于 Web 应用程序漏洞及其缓解技术的信息。

关于 Node.js 安全的额外资源

优秀的 Node.js 安全资源