当前位置: 首页 > 科技观察

干货:Node.js安全指南

时间:2023-03-15 00:00:38 科技观察

当项目周期接近尾声时,开发者会越来越重视应用的“安全”。安全的应用程序不是奢侈品,而是必需品。您应该在开发的每个阶段都考虑应用程序安全性,例如系统架构、设计、编码和最终部署。在本教程中,我们将逐步学习如何提高Node.js应用程序的安全性。1.数据验证——永远不要相信您的用户使用来自用户输入或其他系统的数据,您必须对其进行验证。否则,这会对当前系统构成威胁,并导致难以想象的安全漏洞。现在,让我们学习如何在Node.js中验证传入数据。您可以使用一个名为validator的模块来执行数据验证。例如:constvalidator=require('validator');validator.isEmail('foo@bar.com');//=>truevalidator.isEmail('bar.com');//=>false另外,你可以也使用joi模块来验证数据和模型,例如:constjoi=require('joi');try{constschema=joi.object().keys({name:joi.string().min(3).max(45).required(),email:joi.string().email().required(),password:joi.string().min(6).max(20).required()});constdataToValidate={姓名:“Shahid”,电子邮件:“abc.com”,密码:“123456”,}constresult=schema.validate(dataToValidate);if(result.error){throwresult.error.details[0].message;}}赶上(e){console.log(e);}2。SQL注入攻击SQL注入可以允许恶意用户通过传递非法参数来篡改SQL语句。下面是一个例子,假设你写了这样一条SQL:UPDATEusersSETfirst_name="'+req.body.first_name+'"WHEREid=1332;在正常情况下,您希望这个查询是这样的:UPDATEusersSETfirst_name="John"WHEREid=1332;但是现在,如果有人通过下面的方式传递first_name的值:John",last_name="Wick";--这时候你的SQL语句就会变成这样:UPDATEusersSETfirst_name="John",last_name="Wick";--"WHEREid=1001;你会看到WHERE条件被注释掉了,这次更新会将整个表中所有用户的first_name更改为John,last_name更改为Wick。现在,你有麻烦了!如何避免SQL注入避免SQL注入攻击最有效的方法是过滤输入数据。可以对每一个输入的数据进行逐个验证,也可以使用参数绑定的方式进行验证。开发人员最常用的方法是参数绑定,因为它高效且安全。如果你使用的是一些比较流行的ORM框架,比如sequelize、hibernate等,那么框架中已经提供了这种数据验证和SQL注入保护机制。如果你更喜欢依赖数据库模块,比如Node的mysql,那么你可以使用数据库提供的过滤方法。下面的代码是Node使用mysql的例子:varmysql=require('mysql');varconnection=mysql.createConnection({host:'localhost',user:'me',password:'secret',database:'my_db'});connection.connect();connection.query('UPDATEusersSET??=?WHERE??=?',['first_name',req.body.first_name,,'id',1001],function(err,结果){//...});??的地方替换为字段名,以及?的位置替换为字段值,保证了输入值的安全性。您还可以使用存储过程来提高安全级别,但由于缺乏可维护性,开发人员往往会避免使用它们。同时,您还应该执行服务器端数据验证。但是我不建议大家去手动验证每个字段,可以使用joi等模块来解决这个问题。类型转换JavaScript是一种动态类型语言,即值可以是任何类型。可以使用类型转换的方法来校验数据的类型,这样可以保证只有指定类型的数据才能进入数据库。例如用户ID只能是数字类型,见如下代码:varmysql=require('mysql');varconnection=mysql.createConnection({host:'localhost',user:'me',password:'secret',database:'my_db'});connection.connect();connection.query('UPDATEusersSET??=?WHERE??=?',['first_name',req.body.first_name,,'id',Number(req.body.ID)],function(err,result){//...});你注意到变化了吗?这里我们使用Number(req.body.ID)方法来保证用户ID必须是一个数字。3.应用认证与授权敏感数据(如密码)应安全地存储在系统中,以防止恶意用户滥用敏感信息。在本节中,我们将学习如何存储和管理常用密码,几乎每个应用程序在其系统中都有不同的密码存储方式。密码散列散列是一种接受输入值并生成固定大小字符串的函数。哈希函数的输出值无法解密,可以说是“单向”。因此,对于密码这样的数据,存储在数据库中的值必须是哈希值,而不是明文。您可能想知道,由于散列是一种不可逆的加密形式,攻击者如何获得密码的访问权限?正如我上面提到的,散列加密采用输入字符串并产生固定长度的输出值。于是攻击者反其道而行之,他们从一个规则的密码列表中生成一个哈希值,然后将这个哈希值与系统中的哈希值进行比较,从而找到密码。这种类型的攻击称为查找表,这就是为什么您作为系统架构师决不允许在系统中使用简单的通用密码。为了避免攻击,您还可以使用一种叫做“盐”的东西,我们称之为“散列和加盐”。将盐附加到密码哈希,从而使输入值唯一。salt值必须是随机且不可预测的。我们推荐您使用的哈希算法是BCrypt,在Node.js中您可以使用bcyrpt节点模块来执行哈希。请参考下面例子中的代码:constbcrypt=require('bcrypt');constsaltRounds=10;constpassword="Some-Password@2020";bcrypt.hash(password,saltRounds,(err,passwordHash)=>{//wewilljustprintittotheconsolefornow//youshouldstoreitssomewhereandneverlogsorprintitconsole.log("HashedPassword:",passwordHash);});SaltRounds函数是哈希函数的代价,代价越高,生成的哈希密码越安全。您应该根据服务器的计算能力来确定盐值。生成密码的哈希值后,会将用户输入的密码与存储在数据库中的哈希值进行比对。参考代码如下:constbcrypt=require('bcrypt');constincomingPassword="Some-Password@2020";constexistingHash="some-hash-previously-generated"bcrypt.compare(incomingPassword,existingHash,(err,res)=>{if(res&&res===true){returnconsole.log("ValidPassword");}//invalidpasswordhandlinghereelse{console.log("InvalidPassword");}});密码存储无论使用数据库还是文件存储密码,都不能使用明文存储。正如我们在上一节中了解到的,您可以散列密码并将它们存储在数据库中。我建议对密码字段使用varchar(255)数据类型,但您也可以选择无限长度的字段类型。如果您使用的是bcrypt,则可以使用varchar(60)字段类型,因为bcrypt生成的哈希值固定长度为60个字符。使用适当的角色权限对系统进行身份验证和授权将防止一些恶意用户在系统中执行超出其权限的操作。为了实施适当的授权过程,为每个用户分配适当的角色和权限,以便他们可以在其权限范围内执行某些任务。在Node.js中,你可以使用著名的ACL模块来开发基于系统中权限的访问控制列表。constACL=require('acl2');constacl=newACL(newACL.memoryBackend());//guestisallowedtoviewblogsacl.allow('guest','blogs','view')//checkifthepermissionisgrantedacl.isAllowed('joed','blogs','view',(err,res)=>{if(res){console.log("Userjoedisallowedtoviewblogs");}});请查阅acl2文档以获取更多信息和示例代码。4.暴力攻击防护黑客经常使用软件反复使用不同的密码来尝试获取系统权限,直到找到有效密码。这种攻击方法称为蛮力攻击。避免这种攻击的一种简单有效的方法是“让他稍等一下”,即当有人试图登录系统并输入无效密码超过3次时,要求他们等待60秒左右,然后重试。这样一来,攻击者的时间成本就会大大增加,并且使他们永远无法破解密码。防止这种攻击的另一种方法是阻止无效登录请求的IP。系统允许每个IP在24小时内进行3次错误登录尝试。如果有人试图暴力破解,他们的IP将被封锁24小时。许多公司都使用这种方法来防止暴力攻击。如果使用Express框架,则有一个中间件模块可以对传入请求进行速率限制。它叫做express=brute。下面是一个例子。安装依赖项npminstallexpress-brute--save在路由中启用它constExpressBrute=require('express-brute');conststore=newExpressBrute.MemoryStore();//在本地存储状态,不要在生产中使用这个constbruteforce=newExpressBrute(store);app.post('/auth',bruteforce.prevent,//error429ifwehitthisroutetoooftenfunction(req,res,next){res.send('Success!');});//...5.HTTPS安全传输现在是2021,您还应该使用HTTPS向网络发送数据。HTTPS是HTTP协议的扩展,支持安全通信。使用HTTPS可以保证用户在互联网上发送的数据是加密的、安全的。我不打算在这里详细介绍HTTPS协议的工作原理,我们只讨论如何使用它。在这里,我强烈建议使用LetsEncrypt为您的所有域生成安全证书。您可以将LetsEncrypt与基于Apache和Nginx的Web服务器一起使用。我强烈建议你在反向代理或网关层使用HTTPS协议,因为它们有很多繁重的计算操作。6.会话劫持保护会话(session)是任何动态web应用中最重要的部分,一个安全的会话对于用户和系统来说都是非常必要的。会话是使用cookie实现的,因此必须对其进行保护以防止会话劫持。以下是可以为每个cookie设置的属性列表及其含义:secure-此属性告诉浏览器仅在通过HTTPS发送请求时才发送cookie。HttpOnly-此属性用于防止跨站点脚本攻击,因为它不允许通过JavaScript访问cookie。域-此属性用于与请求URL的服务器的域名进行比较。如果域名匹配,或者是其子域,则接下来检查路径属性。path-除了domian,你还可以指定cookie有效的URL路径。如果域和路径匹配,则可以将cookie与请求一起发送。expires-该属性用于设置一个持久性cookie,cookie只会在设置的日期后过期。在Express框架中,您可以使用express-sessionnpm模块来管理会话。constxpress=require('express');constsession=require('express-session');constapp=express();app.use(session({secret:'keyboardcat',resave:false,saveUninitialized:true,cookie:{安全:真实,路径:'/'}}));7、跨站请求伪造攻击(CSRF)防护跨站请求伪造攻击利用系统中受信任的用户对Web应用程序进行有害的恶意操作。在Node.js中,我们可以使用csurf模块来缓解CSRF攻击。该模块需要先初始化express-session或者cookie-parser,可以看下面的示例代码:constexpress=require('express');constcookieParser=require('cookie-parser');constcsrf=require('csurf');constbodyParser=require('body-parser');//setupproutemiddlewaresconstcsrfProtection=csrf({cookie:true});constparseForm=bodyParser.urlencoded({extended:false});//createexpressappconstapp=express();//我们需要这个因为"cookie"istrueiincsrfProtectionapp.use(cookieParser());app.get('/form',csrfProtection,function(req,res){//passthecsrfTokentotheviewres.render('send',{csrfToken:req.csrfToken()});});app.post('/process',parseForm,csrfProtection,function(req,res){res.send('dataisbeingprocessed');});app.listen(3000);在页面中,需要创建一个隐藏输入字段,在输入字段中保存CSRF令牌,例如:Favoritecolor:提交如果你使用的是AJAX请求,那么可以通过请求头(header)传递CSRFtokenvartoken=document.querySelector('meta[name="csrf-token"]').getAttribute('content');headers:{'CSRF-Token':token}8.Denialofservice拒绝服务或DOS攻击,允许攻击者破坏系统,使系统被迫关闭服务或用户无法访问服务。攻击者经常向系统发送大量流量和请求,从而增加服务器CPU和内存负载,导致系统崩溃。为了减轻Node.js应用程序中的DOS攻击,首先是识别此类事件,我强烈建议将这两个模块集成到系统中。帐户锁定-在n次尝试失败后,锁定帐户或IP地址一段时间(例如24小时?)速率限制-限制用户在一定时间内只能请求系统n次,例如单个用户只能r每分钟请求3次。正则表达式拒绝服务攻击(ReDOS)是DOS攻击的一种,攻击者利用系统中正则表达式的设计缺陷或计算复杂性,消耗大量服务器系统资源,造成服务器服务中断或停止。我们可以使用一些工具来检查有风险的正则表达式,从而避免使用这些正则表达式。例如这个工具:https://github.com/davisjam/vuln-regex-detector9。依赖验证我们都在我们的项目中使用了很多依赖。我们还需要检查和验证这些依赖关系,以确保整个项目的安全。NPM已经有这样的审计功能来发现项目中的漏洞。只需在源代码目录中运行以下命令:npmaudit要修复该漏洞,您可以运行此命令:npmauditfix您也可以在将修复应用到项目之前进行试运行以检查修复。npmauditfix--dry-run--json10。HTTP安全头信息HTTP提供了一些安全头信息来防止常见的攻击。如果你使用的是Express框架,你可以使用头盔模块,一行代码就可以启用所有的安全头。npminstallhelmet--save让我们看看如何使用它:constexpress=require("express");consthelmet=require("helmet");constapp=express();app.use(helmet());//...this将启用以下HTTP标头:Strict-Transport-SecurityX-frame-OptionsX-XSS-ProtectionX-Content-Type-ProtectionContent-Security-PolicyCache-ControlExpect-CTDisableX-Powered-By这些HTTP标头防止恶意用户的各种攻击,如点击劫持、跨站脚本攻击等。