项目结构实践
- 组件式构建解决方案
推荐: 通过组件构建解决方案
- components
- orders
- users
- index.js
- user.js
- userService.js
避免: 按照技术角色对文件进行分组
- controllers
- api.js
- home.js
- models
- order.js
- user.js
分层设计组件,保持Express在特定的区域
每一个组件都应该包含层级
,一个专注的用于接入网络,逻辑,数据的概念
经典的分层结构,将代码分类 web, service,和DAL层。每一层次只和上一层的结构打交道公共使用的工具封装成npm包
和java的maven二方包类似使用易于设置环境变量,安全和分级的配置
配置文件类似egg.js 中production.config.js
,dev.config.js
等。可以通过比如rc, nconf, config 和 convict等几种包完成。
错误处理最佳实践
- 使用内建的错误对象
比如
throw new Error("not Provieded");
或者构建自定义的错误对象
//从node错误派生的集中错误对象
function appError(name, httpCode, description, isOperational) {
Error.call(this);
Error.captureStackTrace(this);
this.name = name;
//...在这赋值其它属性
};
appError.prototype = Object.create(Error.prototype);
appError.prototype.constructor = appError;
module.exports.appError = appError;
区分运行错误(runtimeError)和程序设计错误
集中处理错误,并且在特殊情况产生时候,优雅退出服务
process.on('unhandledRejection', (reason, p) => {
//我刚刚捕获了一个未处理的promise rejection, 因为我们已经有了对于未处理错误的后备的处理机制(见下面), 直接抛出,让它来处理
throw reason;
});
process.on('uncaughtException', (error) => {
//我刚收到一个从未被处理的错误,现在处理它,并决定是否需要重启应用
errorManagement.handler.handleError(error);
if (!errorManagement.handler.isTrustedError(error))
process.exit(1);
});
安全最佳实践
- DOS攻击,可以使用cloud负载均衡,防火墙或者nginx等做限流处理,对于小的应用,可以使用限流中间件做限速限制
const http = require('http');
const redis = require('redis');
const { RateLimiterRedis } = require('rate-limiter-flexible');
const redisClient = redis.createClient({
enable_offline_queue: false,
});
// Maximum 20 requests per second
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 20,
duration: 1,
blockDuration: 2, // block for 2 seconds if consumed more than 20 points per second
});
http.createServer(async (req, res) => {
try {
const rateLimiterRes = await rateLimiter.consume(req.socket.remoteAddress);
// Some app logic here
res.writeHead(200);
res.end();
} catch {
res.writeHead(429);
res.end('Too Many Requests');
}
})
.listen(3000);
- 不要在配置文件或者源代码中存储文本机密信息,使用环境变量或者Docker Secrets等安全管理系统。
const azure = require('azure');
const apiKey = process.env.AZURE_STORAGE_KEY;
可以使用git commit来审计提交和提交消息,以便意外添加秘密,例如git-secrets.
- 使用ORM库防止SQL注入的漏洞
- TypeORM
- sequelize
- mongoose
- 通用安全最佳实践集合
- 使用SSL/TLS 加密客户端服务器连接,避免中间人攻击,监控用户的行为
- 使用加密存储信息
- 参考OWASP建议 https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#Implement_Proper_Password_Strength_Controls.22
调整HTTP响应头加强安全性
用程序应该使用安全的header来防止攻击者使用常见的攻击方式,诸如跨站点脚本(XSS)、点击劫持和其他恶意攻击。可以使用模块,比如 helmet轻松进行配置
参考:
https://github.com/goldbergyoni/nodebestpractices/blob/master/sections/security/secureheaders.md经常自动检查易受攻击的依赖库
在npm的生态系统中, 一个项目有许多依赖是很常见的。在找到新的漏洞时, 应始终将依赖项保留在检查中。使用工具,类似于npm audit 或者 snyk跟踪、监视和修补易受攻击的依赖项。将这些工具与 CI 设置集成, 以便在将其上线之前捕捉到易受攻击的依赖库。避免使用Node.js的crypto库处理密码,使用Bcrypt
当存储用户密码的时候,建议使用bcrypt npm module提供的自适应哈希算法bcrypt,而不是使用Node.js的crypto模块。由于Math.random()
的可预测性,它也不应该作为密码或者令牌生成的一部分。转义 HTML、JS 和 CSS 输出
发送给浏览器的不受信任数据可能会被执行, 而不是显示, 这通常被称为跨站点脚本(XSS)攻击。使用专用库将数据显式标记为不应执行的纯文本内容(例如:编码、转义),可以减轻这种问题。验证传入的JSON schemas
验证传入请求的body payload,并确保其符合预期要求, 如果没有, 则快速报错。为了避免每个路由中繁琐的验证编码, 您可以使用基于JSON的轻量级验证架构,比如jsonschema or joi支持黑名单的JWT
当使用JSON Web Tokens(例如, 通过Passport.js), 默认情况下, 没有任何机制可以从发出的令牌中撤消访问权限。一旦发现了一些恶意用户活动, 只要它们持有有效的标记, 就无法阻止他们访问系统。通过实现一个不受信任令牌的黑名单,并在每个请求上验证,来减轻此问题。限制每个用户允许的登录请求
使用诸如/login,/admin之类的较高权限的路由而没有使用速率限制。使得应用程序面临暴力密码字典攻击的风险。可以基于请求属性(比如IP) 或者主体参数比如电子邮件等,限制允许尝试的次数。
const maxWrongAttemptsByIPperDay = 100;
const maxConsecutiveFailsByUsernameAndIP = 10;
const limiterSlowBruteByIP = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'login_fail_ip_per_day',
points: maxWrongAttemptsByIPperDay,
duration: 60 * 60 * 24,
blockDuration: 60 * 60 * 24, // Block for 1 day, if 100 wrong attempts per day
});
const limiterConsecutiveFailsByUsernameAndIP = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'login_fail_consecutive_username_and_ip',
points: maxConsecutiveFailsByUsernameAndIP,
duration: 60 * 60 * 24 * 90, // Store number for 90 days since first fail
blockDuration: 60 * 60, // Block for 1 hour
});
- 使用非root用户运行Nodejs
Node.js作为一个具有无限权限的root用户运行,使得攻击者在本地计算机获取无限制的权利,比如(改变iptable,引流到他的服务器上)
但是有两个常见场景需要root权限。
- 运行特权端口比如80端口
- Docker容器默认以root运行,建议使用nginx反向代理进行转发到Node程序,然后监听非特权端口。
非root权限制作dockerfile镜像以
FROM node:latest
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]
- 使用反向代理或者中间件限制负载大小
请求Body的有效负载荷越大,Node单线程就越难处理,使得攻击者没有大量请求(DOS/DDOS)情况下,可以先让服务器挂掉。,可以在边缘上(SLB,防火墙)等限制请求body的大小。
http {
...
# Limit the body size for ALL incoming requests to 1 MB
client_max_body_size 1m;
}
server {
...
# Limit the body size for incoming requests to this specific server block to 1 MB
client_max_body_size 1m;
}
location /upload {
...
# Limit the body size for incoming requests to this route to 1 MB
client_max_body_size 1m;
}
避免JavaScript的eval声明
evel允许允许时指定自定义javascript代码,不仅仅是性能问题,还是重要的安全问题,因为恶意的js代码可能来源于用户的输入,防止恶意RegEx让Node.js的单线程过载执行
正则表达式在方便的同时,给javascript应用造成真正的威胁,特别在nodejs平台。匹配文本的用户输入需要大量的CPU周期来处理。偏向第三方的验证包,比如validator.js,而不是采用正则,或者使用safe-regex来检测有问题的正则表达式。使用变量避免模块加载
避免使用被指定为参数的路径变量导入(requiring/importing)另一个文件, 因为该变量可能源自用户输入。此规则可以扩展到一般情况下的访问文件(例如,fs.readFile()),或者包含源自用户输入的动态变量的其他敏感资源。
// 不安全, 因为helperPath变量可能通过用户输入而改变
const uploadHelpers = require(helperPath);
// 安全
const uploadHelpers = require('./helpers/upload');
-
在沙箱中运行不安全的代码
例如, 考虑一个动态框架(如 webpack), 该框架接受自定义加载器(custom loaders), 并在构建时动态执行这些加载器。在存在一些恶意插件的情况下, 我们希望最大限度地减少损害, 甚至可能让工作流成功终止 - 这需要在一个沙箱环境中运行插件, 该环境在资源、宕机和我们共享的信息方面是完全隔离的。三个主要选项可以帮助实现这种隔离:
- 一个专门的子进程 - 这提供了一个快速的信息隔离, 但要求制约子进程, 限制其执行时间, 并从错误中恢复
- 一个基于云的无服务框架满足所有沙盒要求,但动态部署和调用Faas方法不是本部分的内容
- 一些npm库,比如sandbox和vm2允许通过一行代码执行隔离代码。尽管后一种选择在简单中获胜, 但它提供了有限的保护。
const Sandbox = require("sandbox");
const s = new Sandbox();
s.run( "lol)hai", function( output ) {
console.log(output);
//output='Syntax error'
});
// Example 4 - Restricted code
s.run( "process.platform", function( output ) {
console.log(output);
//output=Null
})
// Example 5 - Infinite loop
s.run( "while (true) {}", function( output ) {
console.log(output);
//output='Timeout'
})
- 隐藏客户端的错误详细信息
尽管子进程非常棒, 但使用它们应该谨慎。如果无法避免传递用户输入,就必须经过脱敏处理。 未经脱敏处理的输入执行系统级逻辑的危险是无限的, 从远程代码执行到暴露敏感的系统数据, 甚至数据丢失。准备工作的检查清单可能是这样的
- 避免在每一种情况下的用户输入, 否则验证和脱敏处理
- 使用user/group标识限制父进程和子进程的权限
- 在隔离环境中运行进程, 以防止在其他准备工作失败时产生不必要的副作用
const { exec } = require('child_process');
...
// 例如, 以一个脚本为例, 它采用两个参数, 其中一个参数是未经脱敏处理的用户输入
exec('"/path/to/test file/someScript.sh" --someOption ' + input);
// -> 想象一下, 如果用户只是输入'&& rm -rf --no-preserve-root /'类似的东西, 会发生什么
// 你会得到一个不想要的结果
隐藏客户端的错误详细信息
您实现自己的错误处理逻辑与自定义错误对象(被许多人认为是最佳做法)。如果这样做, 请确保不将整个Error对象返回到客户端, 这可能包含一些敏感的应用程序详细信息。
否则敏感应用程序详细信息(如服务器文件路径、使用中的第三方模块和可能被攻击者利用的应用程序的其他内部工作流)可能会从stack trace发现的信息中泄露。避免将机密信息发布到NPM仓库
应该采取预防措施来避免偶然地将机密信息发布到npm仓库的风险。 一个.npmignore
文件可以被用作忽略掉特定的文件或目录, 或者一个在 package.json 中的files
数组可以起到一个白名单的作用.
一个. npmignore文件
#tests
test
coverage
#build tools
.travis.yml
.jenkins.yml
#environment
.env
.config