Node.js 应用程序安全指南

2025-05-26

Node.js 应用程序安全指南

开发人员在开发周期结束时往往会考虑的一件事就是应用程序的“安全性”。安全的应用程序并非奢侈品,而是必需品。您应该在开发的每个阶段(例如架构、设计、代码以及最终的部署)考虑应用程序的安全性。

在本教程中,我们将学习如何保护 Node.js 应用程序的安全。让我们开始吧。

数据验证——永远不要相信你的用户

您必须始终验证或清理来自用户或系统其他实体的数据。糟糕的验证或根本没有验证会对系统正常运行构成威胁,并可能导致安全漏洞。您还应该对输出进行转义。让我们学习如何在 Node.js 中验证传入数据。您可以使用名为validator的 Node 模块来执行数据验证。例如。

const validator = require('validator');
validator.isEmail('foo@bar.com'); //=> true
validator.isEmail('bar.com'); //=> false
Enter fullscreen mode Exit fullscreen mode

您还可以使用名为joi的模块(Codeforgeek 推荐)来执行数据/模式验证。例如:

  const joi = require('joi');
  try {
    const schema = joi.object().keys({
      name: joi.string().min(3).max(45).required(),
      email: joi.string().email().required(),
      password: joi.string().min(6).max(20).required()
    });

    const dataToValidate = {
        name: "Shahid",
        email: "abc.com",
        password: "123456",
    }
    const result = schema.validate(dataToValidate);
    if (result.error) {
      throw result.error.details[0].message;
    }    
  } catch (e) {
      console.log(e);
  }
Enter fullscreen mode Exit fullscreen mode

SQL注入攻击

SQL 注入是一种利用漏洞,恶意用户可以传递意外数据并更改 SQL 查询。让我们通过以下示例来理解。假设您的 SQL 查询如下所示:

UPDATE users
    SET first_name="' + req.body.first_name +  '" WHERE id=1332;
Enter fullscreen mode Exit fullscreen mode

在正常情况下,您可能希望此查询如下所示:

UPDATE users
    SET first_name = "John" WHERE id = 1332;
Enter fullscreen mode Exit fullscreen mode

现在,如果有人将 first_name 作为如下所示的值传递:

John", last_name="Wick"; --
Enter fullscreen mode Exit fullscreen mode

然后,您的 SQL 查询将如下所示:

UPDATE users
    SET first_name="John", last_name="Wick"; --" WHERE id=1001;
Enter fullscreen mode Exit fullscreen mode

如果你观察一下,会发现 WHERE 条件被注释掉了,现在查询会更新用户表,并将每个用户的名字设置为“John”,姓氏设置为“Wick”。这最终会导致系统崩溃,如果你的数据库没有备份,那么你就完蛋了。

如何防止SQL注入攻击

防止 SQL 注入攻击最有效的方法是清理输入数据。您可以验证每个输入,也可以使用参数绑定进行验证。参数绑定是开发人员最常用的方法,因为它既高效又安全。如果您使用的是流行的 ORM(例如 Sequelize、Hibernate 等),那么它们已经提供了验证和清理数据的功能。如果您使用的是 ORM 以外的数据库模块(例如Node 的 mysql),则可以使用模块提供的转义方法。让我们通过示例来学习。下面显示的代码库使用的是Node 的mysql模块。

var mysql = require('mysql');
var connection = mysql.createConnection({
  host     : 'localhost',
  user     : 'me',
  password : 'secret',
  database : 'my_db'
});

connection.connect();

connection.query(
    'UPDATE users SET ?? = ? WHERE ?? = ?',
    ['first_name',req.body.first_name, ,'id',1001],
    function(err, result) {
    //...
});
Enter fullscreen mode Exit fullscreen mode

双问号会被替换为字段名称,单问号会被替换为值。这将确保输入安全。您也可以使用存储过程来提高安全性,但由于存储过程缺乏可维护性,开发人员往往避免使用存储过程。您还应该执行服务器端数据验证。我不建议您手动验证每个字段,您可以使用像joi这样的模块。

类型转换

JavaScript 是一种动态类型语言,也就是说,值可以是任意类型。您可以使用类型转换方法来验证数据类型,以便只有预期类型的​​值才能存入数据库。例如,如果用户 ID 只能接受数字,就应该进行类型转换以确保用户 ID 只能是数字。例如,让我们参考上面显示的代码。

var mysql = require('mysql');
var connection = mysql.createConnection({
  host     : 'localhost',
  user     : 'me',
  password : 'secret',
  database : 'my_db'
});

connection.connect();

connection.query(
    'UPDATE users SET ?? = ? WHERE ?? = ?',
    ['first_name',req.body.first_name, ,'id',Number(req.body.ID)],
    function(err, result) {
    //...
});
Enter fullscreen mode Exit fullscreen mode

你注意到变化了吗?我们使用了Number(req.body.ID)来确保 ID 始终是数字。你可以参考一位博主的精彩文章,深入了解类型转换。

应用程序身份验证和授权

密码等敏感数据应以安全的方式存储在系统中,以防止恶意用户滥用敏感信息。在本节中,我们将学习如何存储和管理密码。密码非常通用,几乎每个应用程序在其系统中都以某种方式设置了密码。

密码哈希

哈希函数是一种根据输入生成固定大小字符串的函数。哈希函数的输出无法解密,因此本质上是“单向”的。对于密码之类的数据,您必须始终使用哈希算法来生成输入密码字符串的哈希版本,即明文字符串。

您可能想知道,如果哈希是单向字符串,那么攻击者如何获取密码?

正如我上面提到的,哈希算法接受一个字符串作为输入,并生成一个固定长度的输出。因此,攻击者采取相反的方法,他们根据通用密码列表生成哈希值,然后将该哈希值与系统中的哈希值进行比较,以找到密码。这种攻击称为查找表攻击。

这就是为什么作为系统架构师,您绝不能允许在系统中使用通用密码的原因。为了抵御这种攻击,您可以使用一种叫做“盐”的东西。盐被附加到密码哈希值中,使其无论输入如何都是唯一的。盐必须安全且随机地生成,以确保其不可预测。我们建议您使用BCrypt哈希算法。在撰写本文时,Bcrypt 尚未被利用,并且被认为是安全的加密算法。在 Node.js 中,您可以使用bcyrpt节点模块执行哈希运算。

请参考下面的示例代码。

const bcrypt = require('bcrypt');

const saltRounds = 10;
const password = "Some-Password@2020";

bcrypt.hash(
    password,
    saltRounds,
    (err, passwordHash) => {

    //we will just print it to the console for now
    //you should store it somewhere and never logs or print it

    console.log("Hashed Password:", passwordHash);
});
Enter fullscreen mode Exit fullscreen mode

SaltRounds函数是哈希函数的代价。代价越高,生成的哈希值就越安全。您应该根据服务器的计算能力来决定盐值。生成密码哈希值后,系统会将用户输入的密码与数据库中存储的哈希值进行比较。请参阅以下代码。

const bcrypt = require('bcrypt');

const incomingPassword = "Some-Password@2020";
const existingHash = "some-hash-previously-generated"

bcrypt.compare(
    incomingPassword,
    existingHash,
    (err, res) => {
        if(res && res === true) {
            return console.log("Valid Password");
        }
        //invalid password handling here
        else {
            console.log("Invalid Password");
        }
});
Enter fullscreen mode Exit fullscreen mode

密码存储

无论您使用数据库还是文件存储密码,都不能存储纯文本版本。正如我们上面所研究的,您应该生成哈希值并将其存储在系统中。对于密码,我通常建议使用varchar(255)数据类型。您也可以选择无限长度的字段。如果您使用bcrypt,则可以使用varchar(60)字段,因为bcrypt会生成固定大小的 60 个字符哈希值。

授权

拥有适当用户角色和权限的系统可以防止恶意用户在其权限范围之外进行操作。为了实现正确的授权流程,需要为每个用户分配适当的角色和权限,以便他们能够执行特定任务,而仅此而已。在 Node.js 中,您可以使用一个名为ACL的著名模块来在系统中基于授权开发访问控制列表。

const ACL = require('acl2');
const acl = new ACL(new ACL.memoryBackend());
// guest is allowed to view blogs
acl.allow('guest', 'blogs', 'view')
// check if the permission is granted
acl.isAllowed('joed', 'blogs', 'view', (err, res) => {
    if(res){
        console.log("User joed is allowed to view blogs");
    }
});
Enter fullscreen mode Exit fullscreen mode

查看 acl2 文档以获取更多信息和示例代码。

暴力攻击预防

暴力破解是一种黑客使用软件反复尝试不同密码,直到获得访问权限(即找到有效密码)的攻击方式。为了防止暴力破解攻击,最简单的方法之一就是等待。当有人尝试登录您的系统并尝试超过3次无效密码时,让他们等待60秒左右再尝试。这样一来,攻击者的攻击速度就会变慢,破解密码将需要很长时间。

另一种防止这种情况发生的方法是封禁生成无效登录请求的 IP。您的系统允许每个 IP 在 24 小时内尝试登录 3 次。如果有人尝试暴力破解,则封禁该 IP 24 小时。许多公司都使用这种限速方法来防止暴力破解攻击。如果您使用的是 Express 框架,则有一个中间件模块可以启用传入请求的限速功能。它名为express=brute

您可以查看下面的示例代码。

安装依赖项。

npm install express-brute --save
Enter fullscreen mode Exit fullscreen mode

在您的路线中启用它。

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!');
    }
);
//...
Enter fullscreen mode Exit fullscreen mode

示例代码取自express-brute模块文档。

使用 HTTPS 进行安全传输

现在是 2021 年,您必须使用 HTTPS 来安全地在互联网上发送数据和流量。HTTPS 是 HTTP 协议的扩展,支持安全通信。使用 HTTPS,您可以确保互联网上的流量和用户数据是加密且安全的。

我不会在这里详细解释 HTTPS 的工作原理。我们将重点介绍它的实现部分。我强烈建议您使用LetsEncrypt为您的所有域/子域生成 SSL 证书。

它是免费的,并运行守护程序每 90 天更新一次 SSL 证书。您可以在此处了解更多关于 LetsEncrypt 的信息。如果您有多个子域名,您可以选择域名专用证书或通配符证书。LetsEncrypt 两种证书都支持。

您可以将 LetsEncrypt 用于基于 Apache 和 Nginx 的 Web 服务器。我强烈建议在反向代理或网关层执行 SSL 协商,因为这是一项计算量很大的操作。

会话劫持预防

会话是任何动态 Web 应用程序的重要组成部分。为了确保用户和系统安全,在应用程序中建立安全的会话至关重要。会话是通过 Cookie 实现的,因此必须确保其安全,以防止会话劫持。以下列出了每个 Cookie 可以设置的属性及其含义:

  • 安全- 此属性告诉浏览器仅当请求通过 HTTPS 发送时才发送 cookie。
  • HttpOnly——此属性用于帮助防止跨站点脚本等攻击,因为它不允许通过 JavaScript 访问 cookie。
  • domain - 此属性用于与请求 URL 的服务器域名进行比较。如果域名匹配或为子域名,则接下来检查 path 属性。
  • 路径- 除了域名之外,还可以指定 Cookie 有效的 URL 路径。如果域名和路径匹配,则 Cookie 将在请求中发送。
  • expires - 此属性用于设置持久性 cookie,因为 cookie 直到超过设置的日期才会过期

您可以使用express-session npm 模块在 Express 框架中执行会话管理。

const express = require('express');
const session = require('express-session');
const app = express();

app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: true, path: '/'}
}));
Enter fullscreen mode Exit fullscreen mode

您可以在此处了解有关 Express 会话处理的更多信息

跨站请求伪造(CSRF)攻击预防

CSRF 是一种攻击方式,攻击者利用系统中的受信任用户在 Web 应用程序上执行恶意操作。在 Node.js 中,我们可以使用csurf模块来缓解 CSRF 攻击。此模块需要先初始化express-sessioncookie-parser。您可以查看下面的示例代码。

const express = require('express');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
const bodyParser = require('body-parser');

// setup route middlewares
const csrfProtection = csrf({ cookie: true });
const parseForm = bodyParser.urlencoded({ extended: false });

// create express app
const app = express();

// we need this because "cookie" is true in csrfProtection
app.use(cookieParser());

app.get('/form', csrfProtection, function(req, res) {
  // pass the csrfToken to the view
  res.render('send', { csrfToken: req.csrfToken() });
});

app.post('/process', parseForm, csrfProtection, function(req, res) {
  res.send('data is being processed');
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

在网页上,您需要创建一个隐藏的输入类型,其值为 CSRF 令牌。例如:

<form action="/process" method="POST">
  <input type="hidden" name="_csrf" value="{{csrfToken}}">

  Favorite color: <input type="text" name="favoriteColor">
  <button type="submit">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

对于 AJAX 请求,您可以在标头中传递 CSRF 令牌。

var token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
  headers: {
    'CSRF-Token': token
  }
Enter fullscreen mode Exit fullscreen mode

拒绝服务

拒绝服务攻击(DOS)是一种攻击类型,攻击者试图通过破坏系统来瘫痪服务或使用户无法访问服务。攻击者通常会向系统注入大量流量或请求,从而增加 CPU 和内存负载,最终导致系统崩溃。为了缓解 Node.js 应用程序中的 DOS 攻击,第一步是识别此类事件。我强烈建议将这两个模块集成到系统中。

  1. 帐户锁定 - n 次尝试失败后,锁定帐户或 IP 地址一段时间(比如 24 小时?)
  2. 速率限制 - 限制用户在特定时间段内向系统发出 n 次请求,例如,单个用户每分钟发出 3 次请求

正则表达式拒绝服务攻击 (ReDOS) 是一种 DOS 攻击,攻击者利用系统中正则表达式的实现。某些正则表达式的执行需要大量的计算资源,攻击者可以通过向系统提交涉及正则表达式的请求来利用它,从而增加系统负载,导致系统崩溃。您可以使用类似的软件检测危险的正则表达式,并避免在系统中使用它们。

依赖项验证

我们项目中都使用了大量依赖项。为了确保整个项目的安全性,我们需要检查并验证这些依赖项。NPM 已经提供了审计功能,可以查找项目的漏洞。只需在源代码目录中运行以下命令即可。

npm audit
Enter fullscreen mode Exit fullscreen mode

要修复此漏洞,您可以运行此命令。

npm audit fix
Enter fullscreen mode Exit fullscreen mode

您还可以在将修复应用到项目之前运行试运行来检查修复效果。

npm audit fix --dry-run --json
Enter fullscreen mode Exit fullscreen mode

HTTP 安全标头

HTTP 提供了多个安全标头,可以防御常见的攻击。如果您使用的是 Express 框架,则可以使用名为Helmet的模块,只需一行代码即可启用所有安全标头。

npm install helmet --save
Enter fullscreen mode Exit fullscreen mode

以下是使用方法。

const express = require("express"); 
const helmet = require("helmet");  
const app = express(); 
app.use(helmet());  
//...
Enter fullscreen mode Exit fullscreen mode

这将启用以下 HTTP 标头。

  • 严格传输安全
  • X-frame-选项
  • X-XSS 保护
  • X-内容类型保护
  • 内容安全策略
  • 缓存控制
  • 预期-CT
  • 禁用 X-Powered-By

这些标头可防止恶意用户遭受各种类型的攻击,例如点击劫持、跨站点脚本等。

作者博客:https://shaikhshahid.com

文章来源:https://dev.to/shaikhshahid/a-guide-to-securing-node-js-applications-4bcc
PREV
Google Keep Lite - 使用 reactjs 构建 Google Keep 克隆版
NEXT
完整路线图:如何开始前端开发?