当项目周期快结束时,开发人员会越来越关注应用的“安全性”问题。一个安全的应用程序并不是一种奢侈,而是必要的。你应该在开发的每个阶段都考虑应用程序的安全性,例如系统架构、设计、编码,包括最后的部署。 在这篇教程中,我们将一步步来学习如何提高Node.js应用程序安全性的方法。 1. 数据验证 - 永远不要信任你的用户来自用户输入或其他系统的数据,你都必须要进行验证。否则,这会对当前系统造成威胁,并导致不可想象的安全漏洞。现在,让我们学习如何验证Node.js中的传入数据。你可以使用名为 const validator = require('validator'); validator.isEmail('foo@bar.com'); //=> true validator.isEmail('bar.com'); //=> false 另外,你也可以使用 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); } 2. 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; 但是现在,如果有人将 John", last_name="Wick"; -- 这时,你的SQL语句就会变成这样: UPDATE users SET first_name="John", last_name="Wick"; --" WHERE id=1001; 你会看到, 如何避免SQL注入避免SQL注入攻击最有效的办法就是将输入数据进行过滤。你可以对每一个输入数据逐一进行验证,也可以用参数绑定的方式验证。开发者们最常用的就是参数绑定的方式,因为它高效而且安全。 如果你在使用一些比较流行的ORM框架,例如sequelize、hibernate等等,那么框架中就已经提供了这种数据验证和SQL注入保护机制。 如果你更喜欢依赖数据库模块,例如 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) { //... });
你也可以使用存储过程来提高安全级别,但是由于缺乏可维护性,开发人员倾向于避免使用存储过程。 同时,你还应该执行服务器端的数据验证。 但我不建议你手动验证每个字段,可以使用 类型转换JavaScript是一种动态类型语言,即值可以是任何类型。你可以使用类型转换方法来验证数据的类型,这样就能保证,只有指定类型的数据才可以进入数据库。比如,用户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) { //... }); 你注意到变化了吗?这里我们使用了 3. 应用程序认证和授权敏感数据(例如密码)应该以一种安全的方式存储在系统中,这样,恶意用户就不会滥用敏感信息。在本节中,我们将学习如何存储和管理通用的密码,几乎每个应用程序在其系统中都有不同的密码存储方式。 密码哈希哈希是一个将输入值生成固定大小字符串的函数。哈希函数的输出值是无法解密的,因此可以说是“单向的”。因此,像密码这样的数据,存储在数据库中的值必须是哈希值,而不是明文。 你也许想知道,既然哈希是一种不可逆的加密方式,那么攻击者又是如何取得密码访问权限的呢? 正如我上面提到的那样,哈希加密使用输入字符串并生成固定长度的输出值。因此,攻击者采取了相反的方法,他们从常规密码列表中生成哈希,然后将哈希与系统中的哈希进行比较以找到密码。这种攻击方式叫做查表法(Lookup Tables) 这就是为什么你作为系统架构师,绝不允许系统中使用简单通用密码的原因。为了避免攻击,你也可以使用一种叫”salt"的东西,我们称之为“哈希加盐法”。将salt附加到密码哈希中,从而使输入值唯一。salt值必须是随机的不可预测的。我们建议你使用的哈希算法是 请参考下面例子中的代码: 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); });
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"); } }); 密码存储无论你是用数据库还是文件来存储密码,都不能使用明文存储。我们在上一节已经学到,你可以将密码进行哈希后存储在数据库中。我推荐密码字段用 认证和授权一个拥有合适的角色权限系统,将会阻止一些恶意用户在系统中做一些越权的事情。为了实现正确的授权过程,将合适的角色和权限分配给每个用户,以便他们可以执行权限范围内的某些任务。在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文档以获取更多信息和示例代码。 4. 暴力攻击防护黑客经常会使用软件反复使用不同的密码尝试获得系统权限,直到找到有效密码为止,这种攻击方式叫做暴力攻击。为了避免这种攻击,一种简单有效的办法是“让他等一会”,也就是说,当某人尝试登录系统并尝试输入无效密码3次以上时,请让他们等待60秒左右,然后再尝试。这样,攻击者将大大提高时间成本,并且将使他们永远无法破解密码。 防止这种攻击的另一种方法是屏蔽无效登录请求的IP。系统在24小时内允许每个IP进行3次错误地登录尝试。如果有人尝试进行暴力破解,则将其IP封锁24小时。 许多公司已使用这种方法来防止暴力攻击。 如果使用Express框架,则有一个中间件模块可在传入请求中启用速率限制。 它称为 下面是一个例子。 安装依赖项 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!'); } ); //... 5. HTTPS安全传输现在已经2021年了,你也应该使用HTTPS来向网络中发送数据了。HTTPS是具有安全通信支持的HTTP协议的扩展。使用HTTPS,可以保证用户在互联网中发送的数据是被加密的,是安全的。 在这里我不打算详细介绍HTTPS协议的工作原理,我们只讨论如何使用它。这里我强烈推荐使用 你可以在基于Apache和Nginx的Web服务器上使用LetsEncrypt。我强烈建议你在反向代理或网关层上使用HTTPS协议,因为它们有很多繁重的计算操作。 6. 会话劫持保护会话(session)是任何动态Web应用程序最重要的部分,一个安全的会话对用户和系统来说真的是非常必要的。会话是使用Cookie实现的,因此必须确保其安全以防止会话劫持。以下是可以为每个cookie设置的属性列表以及它们的含义:
在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: '/'} })); 7. 跨站点请求伪造攻击(CSRF)防护跨站点请求伪造攻击利用系统中受信任的用户,对Web应用程序执行有害的恶意操作。在Node.js中,我们可以使用 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令牌可以通过请求头(header)来传递。 var token = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); headers: { 'CSRF-Token': token } 8. 拒绝服务拒绝服务或DOS攻击,可以让攻击者通过破坏系统,使系统被迫关闭服务或用户无法访问服务。攻击者通常会向系统发送大量的流量和请求,从而增加服务器CPU和内存负载,导致系统崩溃。为了缓解Node.js应用程序中的DOS攻击,首先是要识别此类事件, 我强烈建议将这两个模块集成到系统中。
正则表达式拒绝服务攻击(ReDOS)是DOS攻击的一种,攻击者利用系统中正则表达式的设计缺陷或计算复杂度来大量消耗服务器的系统资源,造成服务器的服务中断或停止。 我们可以使用一些工具来检查有风险的正则表达式,从而避免这些正则表达式的使用。例如这个工具: https://github.com/davisjam/vuln-regex-detector 9. 依赖关系验证我们在项目中都使用了大量的依赖项。我们还需要检查并验证这些依赖关系,以确保整个项目的安全性。NPM已经具有这样的审核功能来查找项目的漏洞。只需在源代码目录中运行下面的命令即可: npm audit 要修复漏洞,可以运行此命令: npm audit fix 您也可以先进行 npm audit fix --dry-run --json 10. HTTP安全头信息HTTP提供了一些安全头信息,可以防止常见的攻击。如果使用的是Express框架,则可以使用 npm install helmet --save 下面来看看如何使用: const express = require("express"); const helmet = require("helmet"); const app = express(); app.use(helmet()); //... 这将启用以下HTTP头:
这些HTTP头可防止恶意用户的各种攻击,例如点击劫持,跨站点脚本攻击等。 (本文完) 公众号 - 前端新世界 |
|