[译]保护Node.js应用程序指南

630 阅读12分钟

原文:A Guide to Securing Node.js Applications 作者:Shahid Shaikh

节点安全

Shahid Shaikh 12月30日・11分钟阅读 开发人员在开发周期结束时倾向于考虑的一件事是应用程序的“安全性”。安全应用程序不是奢侈品,而是必需品。您应该在开发的每个阶段考虑应用程序的安全性,例如架构、设计、代码,最后是部署。 在本教程中,我们将学习保护Node.js应用程序的方法。让我们潜入其中。

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

您必须始终验证或清理来自用户或系统其他实体的数据。错误的验证或根本没有验证是对工作系统的威胁,并可能导致安全漏洞。您还应该转义输出。让我们学习如何验证Node.js.中的传入数据可以使用名为**validator的节点模块来执行数据验证**。例如。

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

您还可以使用名为**joi的模块(**Code for geek推荐)来执行data/schema验证。例如。

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

SQL注入攻击

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

UPDATE users
    SET first_name="' + req.body.first_name +  '" WHERE id=1332;

在正常情况下,您会期望此查询如下所示:

UPDATE users
    SET first_name = "John" WHERE id = 1332;

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

John", last_name="Wick"; --

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

UPDATE users
    SET first_name="John", last_name="Wick"; --" WHERE id=1001;

如果您观察到,WHERE条件将被注释掉,现在查询将更新用户表,并将每个用户的名字设置为“约翰”,姓氏设置为“威克”。这最终会导致系统故障,如果您的数据库没有备份,那么您注定要失败。

如何防止SQL注入攻击

防止SQL注入攻击最有用的方法是清理输入数据。您可以验证每个输入或使用参数绑定进行验证。参数绑定主要由开发人员使用,因为它提供了效率和安全性。如果您正在使用流行的ORM,如续集、休眠等,那么它们已经提供了验证和清理数据的功能。如果您使用的是ORM以外的数据库模块,如mysql for 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) {
    //...
});

双问号替换为字段名称,单问号替换为值。这将确保输入是安全的。您也可以使用存储过程来提高安全性,但是由于缺乏可维护性,开发人员倾向于避免使用存储过程。您还应该执行服务器端数据验证。我不建议你手动验证每个字段,你可以使用**像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) {
    //...
});

你注意到变化了吗?我们以前Number(req.body.ID)以确保ID始终是数字。大家可以参考同行博主这篇美文,深入了解打字。

应用程序认证和授权

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

密码哈希

哈希是一个从输入生成固定大小字符串的函数。哈希函数的输出无法解密,因此它本质上是单向的。对于密码等数据,您必须始终使用哈希算法来生成输入密码字符串的哈希版本,该版本是明文字符串。 你可能会想,如果哈希是单向字符串,那么攻击者为什么会获得密码? 正如我在上面提到的,散列接受一个输入字符串并生成一个固定长度的输出。所以攻击者采取相反的方法,他们从一般密码列表中生成哈希,然后他们将哈希与您系统中的哈希进行比较,以找到密码。这种攻击称为查找表攻击。 这就是为什么作为系统架构师,您必须不允许在系统中使用通用密码的原因。为了克服这种攻击,你可以用一种叫做“”的东西。Salt附加到密码哈希,使其无论输入是什么都是唯一的。盐必须是安全和随机产生的,这样它是不可预测的。我们建议您使用的哈希算法是BCrypt。在撰写本文时,Bcrypt尚未被利用,并被认为是加密安全的。在Node.js,可以使用bcyr pt节点模块来执行散列。 请参考下面的示例代码。

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

Salt Round函数是哈希函数的开销。成本越高,生成的哈希就越安全。您应该根据服务器的计算能力来决定。一旦为密码生成哈希,用户输入的密码将与存储在数据库中的哈希进行比较。参考下面的代码。

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

密码存储

无论您使用数据库、文件来存储密码,都不能存储纯文本版本。如上所述,您应该生成哈希并将哈希存储在系统中。我通常建议在密码的情况下使用varchar**(255)数据类型。您也可以选择无限长度的字段。如果您使用的是bc rypt,那么您可以使用varchar(60)**字段,因为bc rypt将生成固定大小的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");
    }
});

查看acl2文档,了解更多信息和示例代码。

预防暴力攻击

Bruteforce是一种黑客使用软件重复尝试不同密码的攻击,直到获得访问权限,即找到有效密码。为了防止布鲁特福斯攻击,最简单的方法之一是待攻击结束。当有人试图登录您的系统并尝试了三次以上的无效密码时,请让他们等待60秒左右,然后再尝试。这样攻击者会很慢,他们会永远破解密码。 防止这种情况的另一种方法是禁止生成无效登录请求的IP。您的系统允许在24小时内每个IP进行3次错误尝试。如果有人试图做蛮力,然后阻止IP 24小时。许多公司都使用这种速率限制方法来防止暴力攻击。如果您正在使用Express框架,则有一个中间件模块可以在传入请求中启用速率限制。它被称为Express=Brute。 您可以查看下面的示例代码。 安装依赖项。

npm install express-brute --save

在您的路线中启用它。

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-brute**模块文档。

使用HTTPS进行安全传输

现在是2021年,您必须使用HTTPS安全地通过互联网发送数据和流量。HTTPS是具有安全通信支持的HTTP协议的扩展。通过使用HTTPS,您可以确保互联网上的流量和用户数据是加密和安全的。 我不会在这里详细解释HTTPS是如何工作的。我们将重点关注它的实施部分。我强烈建议您使用Let sEncrypt为所有domain/subdomain.生成SSL证书 它是免费的,并运行一个守护进程,每90天更新一次SSL证书。您可以在这里了解更多关于Let sEncrypt的信息。如果有多个子域,可以选择特定于域的证书或通配符证书。Lets Encrypt支持这两种。 您可以将Let sEncrypt用于基于Apache和Nginx的Web服务器。我强烈建议在反向代理或网关层执行SSL协商,因为这是一个繁重的计算操作。

会话劫持预防

会话是任何动态Web应用程序的重要组成部分。在应用程序中拥有一个安全的会话是用户和系统安全的必要条件。会话是使用cookie实现的,必须保持安全以防止会话劫持。以下是可以为每个cookie设置的属性及其含义的列表:

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

您可以使用快速会话npm模块在快速框架中执行会话管理。

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: '/'}
}));

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

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

CSRF是一种攻击,它操纵系统的受信任用户在Web应用程序上执行不必要的恶意操作。在Node.js,我们可以使用csurf模块来减轻CSRF攻击。此模块要求先初始化快速会话cookie解析器。您可以查看下面的示例代码。

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

在网页上,您需要使用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>

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

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

拒绝服务

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

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

正则表达式拒绝服务攻击(ReDOS)是一种DOS攻击,攻击者利用系统中的正则表达式实现进行攻击。一些正则表达式需要大量的计算能力来执行,攻击者可以通过在系统中提交涉及正则表达式的请求来攻击它,这反过来增加了系统的负载,导致系统故障。您可以使用这样的软件来检测危险的正则表达式,并避免在系统中使用它们。

依赖性验证

我们都在项目中使用大量的依赖关系。我们还需要检查和验证这些依赖关系,以确保整个项目的安全性。NPM已经有一个审计功能来发现项目的漏洞。只需在源代码目录中运行下面显示的命令。

npm audit

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

npm audit fix

您也可以在将修复程序应用到您的项目之前运行试运行来检查它。

npm audit fix --dry-run --json

HTTP安全标头

HTTP提供了几个可以防止常见攻击的安全标头。如果您正在使用Express框架,那么您可以使用一个名为helmet的模块,通过一行代码启用所有安全标头。

npm install helmet --save

下面是如何使用它。

const express = require("express"); 
const helmet = require("helmet");  
const app = express(); 
app.use(helmet());  
//...

这将启用以下HTTP标头。

  • 严格-运输-安全
  • X-帧-选项
  • X-XSS-保护
  • X-内容-类型-保护
  • 内容-安全-策略
  • 缓存控制
  • 期待-CT
  • 禁用X供电

这些标头防止恶意用户遭受各种类型的攻击,如点击劫持、跨站点脚本等。 教程链接:codeforgeek.com/a-guide-to-…