内容简介:NodeJS 学习笔记:process.nextTick 方法允许你把一个回调放在下一次时间轮询队列的头上,这意味着可以用来延迟执行,结果是比 setTimeout 更有效率。如果没有提供编码格式,文件操作以及很多网络操作就会将数据作为 Buffer 类型返回。
NodeJS 学习笔记:
- 《Node.js硬实战:115个核心技巧》
- i0natan/nodebestpractices
安装
# 使用 nvm 安装 https://github.com/creationix/nvm#install-script # Git install nvm install nvm alias default # 卸载 pkg 安装版 sudo rm -rf /usr/local/{bin/{node,npm},lib/node_modules/npm,lib/node,share/man/*/node.*}
全局变量
导出导入模块
// module-2.js exports.method = function() { return 'Hello'; }; exports.method2 = function() { return 'Hello again'; }; // module-1.js const module2 = require('./module-2'); console.log(module2.method()); // Hello console.log(module2.method2()); // Hello again
模块成组
// package.json { "name": "group", "main": "./index.js" } // index.js module.exports = { one: require('./one'), two: require('./two') };
路径变量
console.log('__dirname:', __dirname); // 文件夹 console.log('__filename:', __filename); // 文件 path.join(__dirname, 'views', 'view.html'); // 如果不希望自己手动处理 / 的问题,使用 path.join
console
占位符 | 类型 | 例子 |
---|---|---|
%s | String | console.log('%s', 'value') |
%d | Number | console.log('%d', 3.14) |
%j | JSON | console.log('%j', {name: 'Chenng'}) |
process
获取信息
// 获取平台信息 process.arch // x64 process.platform // darwin // 获取内存使用情况 process.memoryUsage(); // 获取命令行参数 process.argv
nextTick
process.nextTick 方法允许你把一个回调放在下一次时间轮询队列的头上,这意味着可以用来延迟执行,结果是比 setTimeout 更有效率。
const EventEmitter = require('events').EventEmitter; function complexOperations() { const events = new EventEmitter(); process.nextTick(function () { events.emit('success'); }); return events; } complexOperations().on('success', function () { console.log('success!'); });
Buffer
如果没有提供编码格式,文件操作以及很多网络操作就会将数据作为 Buffer 类型返回。
toString
默认转为 UTF-8
格式,还支持 ascii
、 base64
等。
data URI
// 生成 data URI const fs = require('fs'); const mime = 'image/png'; const encoding = 'base64'; const base64Data = fs.readFileSync(`${__dirname}/monkey.png`).toString(encoding); const uri = `data:${mime};${encoding},${base64Data}`; console.log(uri); // data URI 转文件 const fs = require('fs'); const uri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...'; const base64Data = uri.split(',')[1]; const buf = Buffer(base64Data, 'base64'); fs.writeFileSync(`${__dirname}/secondmonkey.png`, buf);
events
从 EventEmitter 继承
const EventEmitter = require('events').EventEmitter; const AudioDevice = { play: function (track) { console.log('play', track); }, stop: function () { console.log('stop'); }, }; class MusicPlayer extends EventEmitter { constructor() { super(); this.playing = false; } } const musicPlayer = new MusicPlayer(); musicPlayer.on('play', function (track) { this.playing = true; AudioDevice.play(track); }); musicPlayer.on('stop', function () { this.playing = false; AudioDevice.stop(); }); musicPlayer.emit('play', 'The Roots - The Fire'); setTimeout(function () { musicPlayer.emit('stop'); }, 1000); // 处理异常 // EventEmitter 实例发生错误会发出一个 error 事件 // 如果没有监听器,默认动作是打印一个堆栈并退出程序 musicPlayer.on('error', function (err) { console.err('Error:', err); });
通过 domain 管理异常
- 通过 domain 模块的 create 方法创建实例
- 某个错误已经任何其他错误都会被同一个 error 处理方法处理
- 任何在这个回调中导致错误的代码都会被 domain 覆盖到
const domain = require('domain'); const audioDomain = domain.create(); audioDomain.on('error', function(err) { console.log('audioDomain error:', err); }); audioDomain.run(function() { const musicPlayer = new MusicPlayer(); musicPlayer.play(); });
项目结构
组件式构建
异常处理
捕获未处理
除非开发者记得添加.catch语句,在这些地方抛出的错误都不会被uncaughtException事件处理程序来处理,然后消失掉。
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); } });
Joi 验证参数
const memberSchema = Joi.object().keys({ password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/), birthyear: Joi.number().integer().min(1900).max(2013), email: Joi.string().email(), }); function addNewMember(newMember) { //assertions come first Joi.assert(newMember, memberSchema); //throws if validation fails //other logic here }
Kibana 系统监控
https://github.com/i0natan/nodebestpractices/blob/master/sections/production/smartlogging.chinese.md
上线实践
委托反向代理
Node 处理 CPU 密集型任务,如 gzipping,SSL termination 等,表现糟糕。相反,使用一个真正的中间件服务像 Nginx 更好。否则可怜的单线程 Node 将不幸地忙于处理网络任务,而不是处理应用程序核心,性能会相应降低。
虽然 express.js 通过一些 connect 中间件处理静态文件,但你不应该使用它。Nginx 可以更好地处理静态文件,并可以防止请求动态内容堵塞我们的 node 进程。
# 配置 gzip 压缩 gzip on; gzip_comp_level 6; gzip_vary on; # 配置 upstream upstream myApplication { server 127.0.0.1:3000; server 127.0.0.1:3001; keepalive 64; } #定义 web server server { # configure server with ssl and error pages listen 80; listen 443 ssl; ssl_certificate /some/location/sillyfacesociety.com.bundle.crt; error_page 502 /errors/502.html; # handling static content location ~ ^/(images/|img/|javascript/|js/|css/|stylesheets/|flash/|media/|static/|robots.txt|humans.txt|favicon.ico) { root /usr/local/silly_face_society/node/public; access_log off; expires max; }
检测有漏洞的依赖项
https://docs.npmjs.com/cli/audit
安全
拥护 linter 安全准则
使用安全检验插件 eslint-plugin-security 或者 tslint-config-security 。
使用中间件限制并发请求
DOS 攻击非常流行而且相对容易处理。使用外部服务,比如 cloud 负载均衡, cloud 防火墙, nginx, 或者(对于小的,不是那么重要的app)一个速率限制中间件(比如 express-rate-limit ),来实现速率限制。
const RateLimit = require('express-rate-limit'); // important if behind a proxy // to ensure client IP is passed to req.ip app.enable('trust proxy'); var apiLimiter = new RateLimit({ windowMs: 15*60*1000, // 15 minutes max: 100, }); // only apply to requests that begin with /user/ app.use('/user/', apiLimiter);
纯文本机密信息放置
存储在源代码管理中的机密信息必须进行加密和管理 (滚动密钥(rolling keys)、过期时间、审核等)。使用 pre-commit/push 钩子防止意外提交机密信息。
ORM/ODM 库防止查询注入漏洞
要防止 SQL/NoSQL 注入和其他恶意攻击, 请始终使用 ORM/ODM 或database 库来转义数据或支持命名的或索引的参数化查询, 并注意验证用户输入的预期类型。不要只使用 JavaScript 模板字符串或字符串串联将值插入到查询语句中, 因为这会将应用程序置于广泛的漏洞中。
库:
- TypeORM
- sequelize
- mongoose
- Knex
- Objection.js
- waterline
调整 HTTP 响应头以加强安全性
应用程序应该使用安全的 header 来防止攻击者使用常见的攻击方式,诸如跨站点脚本(XSS)、点击劫持和其他恶意攻击。可以使用模块,比如 helmet 轻松进行配置。
HTTP Strict Transport Security (HSTS)
HTTP 严格传输安全(HSTS)是一种保护网站免受协议降级攻击和 Cookie 劫持的 Web 安全策略机制。声明 Web 浏览器只应使用安全的 HTTPS 连接与其交互,严格传输安全标头接受以秒为单位的最长时间值,以通知浏览器仅使用 HTTPS 访问站点的时间,并可包含子域。
Strict-Transport-Security: max-age=2592000; includeSubDomains
Public Key Pinning for HTTP (HPKP)
HTTP 公钥锁定,可以极大地降低应用程序用户遭受中间人(MITM)攻击和其他虚假身份验证问题的风险。
您应该先看看 Expect-CT
报头,因为它具有从错误配置和其他优势中恢复的高级灵活性。
Public-Key-Pins 接受4个值,一个用于添加证书公钥的pin-sha256值,一个使用SHA256算法散列的值,该值可以针对不同的公钥多次添加;一个最大时间,用于告诉浏览器应用该规则的时间。一个 include deSubDomains 值,用于将此规则应用于所有子域;一个Report-uri 值,用于向给定 URL 报告引脚验证失败。
Public-Key-Pins: pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="; report-uri="http://example.com/pkp-report"; max-age=2592000; includeSubDomains
X-Frame-Options
X-Frame-Options 标头通过声明应用程序是否可以使用框架嵌入到其他(外部)页面上,从而保护应用程序免受 Clickjacking 攻击。
- deny:一般不允许嵌入资源
- sameorigin:允许 host/origin
- allow-from:指定域
X-Frame-Options: deny
X-XSS-Protection
在浏览器中启用 跨站脚本攻击
过滤器。它接受4个参数,0 用于禁用过滤器,1 用于启用过滤器并启用页面的自动过滤,mode=block用于启用过滤器并在检测到XSS攻击时阻止页面呈现(必须使用分号将此参数添加到 1, report=<domainToReport>
才能报告违规行为(必须将此参数添加到 1)
X-XSS-Protection: 1; report=http://example.com/xss-report
X-Content-Type-Options
设置此标头将防止浏览器将文件解释为 HTTP 标头中的内容类型所声明的内容以外的其他内容。
X-Content-Type-Options: nosniff
Referrer-Policy
有 8 个参数:
no-referrer no-referrer-when-downgrade origin origin-when-cross-origin same-origin strict-origin strict-origin-when-cross-origin unsafe-url
Referrer-Policy: no-referrer
Expect-CT
有 3 个参数:
report-uri enforce max-age
Expect-CT: max-age=2592000, enforce, report-uri="https://example.com/report-cert-transparency"
Content-Security-Policy
HTTP Content-Security-Policy 响应标头允许控制允许用户代理为给定页面加载的资源。除了少数例外,策略主要涉及指定务器来源和脚本端点。这有助于防止跨站点脚本攻击(XSS)。
Content-Security-Policy: script-src 'self'
使用 Bcrypt 代替 crypto
密码或机密信息(API 密钥)应该使用安全的 hash+salt 函数( bcrypt )来存储, 因为性能和安全原因, 这应该是其 JavaScript 实现的首选。
// 使用10个哈希回合异步生成安全密码 bcrypt.hash('myPassword', 10, function(err, hash) { // 在用户记录中存储安全哈希 }); // 将提供的密码输入与已保存的哈希进行比较 bcrypt.compare('somePassword', hash, function(err, match) { if(match) { // 密码匹配 } else { // 密码不匹配 } });
转义 HTML、JS 和 CSS 输出
发送给浏览器的不受信任数据可能会被执行, 而不是显示, 这通常被称为跨站点脚本(XSS)攻击。使用专用库将数据显式标记为不应执行的纯文本内容(例如:编码、转义),可以减轻这种问题。
验证传入的 JSON schemas
验证传入请求的 body payload,并确保其符合预期要求, 如果没有, 则快速报错。为了避免每个路由中繁琐的验证编码, 您可以使用基于 JSON 的轻量级验证架构,比如 jsonschema 或 joi
支持黑名单的 JWT
当使用 JSON Web Tokens(例如, 通过 Passport.js), 默认情况下, 没有任何机制可以从发出的令牌中撤消访问权限。一旦发现了一些恶意用户活动, 只要它们持有有效的标记, 就无法阻止他们访问系统。通过实现一个不受信任令牌的黑名单,并在每个请求上验证,来减轻此问题。
const jwt = require('express-jwt'); const blacklist = require('express-jwt-blacklist'); app.use(jwt({ secret: 'my-secret', isRevoked: blacklist.isRevoked })); app.get('/logout', function (req, res) { blacklist.revoke(req.user) res.sendStatus(200); });
限制每个用户允许的登录请求
一类保护暴力破解的中间件,比如 express-brute,应该被用在 express 的应用中,来防止暴力/字典攻击;这类攻击主要应用于一些敏感路由,比如 /admin
或者 /login
,基于某些请求属性, 如用户名, 或其他标识符, 如正文参数等。否则攻击者可以发出无限制的密码匹配尝试, 以获取对应用程序中特权帐户的访问权限。
const ExpressBrute = require('express-brute'); const RedisStore = require('express-brute-redis'); const redisStore = new RedisStore({ host: '127.0.0.1', port: 6379 }); // Start slowing requests after 5 failed // attempts to login for the same user const loginBruteforce = new ExpressBrute(redisStore, { freeRetries: 5, minWait: 5 * 60 * 1000, // 5 minutes maxWait: 60 * 60 * 1000, // 1 hour failCallback: failCallback, handleStoreError: handleStoreErrorCallback }); app.post('/login', loginBruteforce.getMiddleware({ key: function (req, res, next) { // prevent too many attempts for the same username next(req.body.username); } }), // error 403 if we hit this route too often function (req, res, next) { if (User.isValidLogin(req.body.username, req.body.password)) { // reset the failure counter for valid login req.brute.reset(function () { res.redirect('/'); // logged in }); } else { // handle invalid user } } );
使用非 root 用户运行 Node.js
Node.js 作为一个具有无限权限的 root 用户运行,这是一种普遍的情景。例如,在 Docker 容器中,这是默认行为。建议创建一个非 root 用户,并保存到 Docker 镜像中(下面给出了示例),或者通过调用带有"-u username" 的容器来代表此用户运行该进程。否则在服务器上运行脚本的攻击者在本地计算机上获得无限制的权利 (例如,改变 iptable,引流到他的服务器上)
FROM node:latest COPY package.json . RUN npm install COPY . . EXPOSE 3000 USER node CMD ["node", "server.js"]
使用反向代理或中间件限制负载大小
请求 body 有效载荷越大, Node.js 的单线程就越难处理它。这是攻击者在没有大量请求(DOS/DDOS 攻击)的情况下,就可以让服务器跪下的机会。在边缘上(例如,防火墙,ELB)限制传入请求的 body 大小,或者通过配置 express body parser
仅接收小的载荷,可以减轻这种问题。否则您的应用程序将不得不处理大的请求, 无法处理它必须完成的其他重要工作, 从而导致对 DOS 攻击的性能影响和脆弱性。
express:
const express = require('express'); const app = express(); // body-parser defaults to a body size limit of 300kb app.use(express.json({ limit: '300kb' })); // Request with json body app.post('/json', (req, res) => { // Check if request payload content-type matches json // because body-parser does not check for content types if (!req.is('json')) { return res.sendStatus(415); // Unsupported media type if request doesn't have JSON body } res.send('Hooray, it worked!'); }); app.listen(3000, () => console.log('Example app listening on port 3000!'));
nginx:
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; }
防止 RegEx 让 NodeJS 过载
匹配文本的用户输入需要大量的 CPU 周期来处理。在某种程度上,正则处理是效率低下的,比如验证 10 个单词的单个请求可能阻止整个 event loop 长达6秒。由于这个原因,偏向第三方的验证包,比如 validator.js ,而不是采用正则,或者使用 safe-regex 来检测有问题的正则表达式。
const saferegex = require('safe-regex'); const emailRegex = /^([a-zA-Z0-9])(([\-.]|[_]+)?([a-zA-Z0-9]+))*(@){1}[a-z0-9]+[.]{1}(([a-z]{2,3})|([a-z]{2,3}[.]{1}[a-z]{2,3}))$/; // should output false because the emailRegex is vulnerable to redos attacks console.log(saferegex(emailRegex)); // instead of the regex pattern, use validator: const validator = require('validator'); console.log(validator.isEmail('liran.tal@gmail.com'));
在沙箱中运行不安全代码
当任务执行在运行时给出的外部代码时(例如, 插件), 使用任何类型的沙盒执行环境保护主代码,并隔离开主代码和插件。这可以通过一个专用的过程来实现 (例如:cluster.fork()), 无服务器环境或充当沙盒的专用 npm 包。
- 一个专门的子进程 - 这提供了一个快速的信息隔离, 但要求制约子进程, 限制其执行时间, 并从错误中恢复
- 一个基于云的无服务框架满足所有沙盒要求,但动态部署和调用Faas方法不是本部分的内容
- 一些 npm 库,比如 sandbox 和 vm2 允许通过一行代码执行隔离代码。尽管后一种选择在简单中获胜, 但它提供了有限的保护。
const Sandbox = require("sandbox"); const s = new Sandbox(); s.run( "lol)hai", function( output ) { console.log(output); //output='Synatx 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' })
隐藏客户端的错误详细信息
默认情况下, 集成的 express 错误处理程序隐藏错误详细信息。但是, 极有可能, 您实现自己的错误处理逻辑与自定义错误对象(被许多人认为是最佳做法)。如果这样做, 请确保不将整个 Error 对象返回到客户端, 这可能包含一些敏感的应用程序详细信息。否则敏感应用程序详细信息(如服务器文件路径、使用中的第三方模块和可能被攻击者利用的应用程序的其他内部工作流)可能会从 stack trace 发现的信息中泄露。
// production error handler // no stacktraces leaked to user app.use(function(err, req, res, next) { res.status(err.status || 500); res.render('error', { message: err.message, error: {} }); });
对 npm 或 Yarn,配置 2FA
开发链中的任何步骤都应使用 MFA(多重身份验证)进行保护, npm/Yarn 对于那些能够掌握某些开发人员密码的攻击者来说是一个很好的机会。使用开发人员凭据, 攻击者可以向跨项目和服务广泛安装的库中注入恶意代码。甚至可能在网络上公开发布。在 npm 中启用两层身份验证(2-factor-authentication), 攻击者几乎没有机会改变您的软件包代码。
https://itnext.io/eslint-backdoor-what-it-is-and-how-to-fix-the-issue-221f58f1a8c8
session 中间件设置
每个 web 框架和技术都有其已知的弱点,告诉攻击者我们使用的 web 框架对他们来说是很大的帮助。使用 session 中间件的默认设置, 可以以类似于 X-Powered-Byheader
的方式向模块和框架特定的劫持攻击公开您的应用。尝试隐藏识别和揭露技术栈的任何内容(例如:Nonde.js, express)。否则可以通过不安全的连接发送cookie, 攻击者可能会使用会话标识来标识web应用程序的基础框架以及特定于模块的漏洞。
// using the express session middleware app.use(session({ secret: 'youruniquesecret', // secret string used in the signing of the session ID that is stored in the cookie name: 'youruniquename', // set a unique name to remove the default connect.sid cookie: { httpOnly: true, // minimize risk of XSS attacks by restricting the client from reading the cookie secure: true, // only send cookie over https maxAge: 60000*60*24 // set cookie expiry length in ms } }));
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。