跳到内容

安全最佳实践

【Security Best Practices】

意图

【Intent】

本文件旨在扩展当前的 威胁模型,并提供关于如何保护 Node.js 应用的详细指南。

【This document intends to extend the current threat model and provide extensive guidelines on how to secure a Node.js application.】

文档内容

【Document Content】

  • 最佳实践:以简化的方式查看最佳实践。我们可以以 这个问题这个指南 作为起点。需要注意的是,此文档针对 Node.js,如果你在寻找更广泛的内容,可以考虑 OSSF最佳实践
  • 攻击说明:用通俗易懂的英语说明并记录,并尽可能提供一些代码示例,展示我们在威胁模型中提到的攻击。
  • 第三方库:定义威胁(拼写仿冒攻击、恶意软件包等)以及有关节点模块依赖的最佳实践等。

威胁列表

【Threat List】

Node.js 威胁模型 定义了在 Node.js 本身中什么被认为是或不被认为是漏洞。根据该模型,下面的一些主题并不是 Node.js 核心中的漏洞,但它们仍然是构建和运行 Node.js 软件时需要考虑的重要应用级威胁。

【The Node.js threat model defines what is or is not considered a vulnerability in Node.js itself. Some of the topics below are not vulnerabilities in Node.js core according to that model, but they are still important application-level threats that you should account for when building and operating Node.js software.】

HTTP 服务器拒绝服务攻击(CWE-400)

【Denial of Service of HTTP server (CWE-400)】

这是一种攻击,应用由于处理传入 HTTP 请求的方式而无法执行其设计的功能。这些请求不必由恶意行为者故意构造:一个配置错误或存在漏洞的客户端也可以向服务器发送一系列请求,从而导致拒绝服务。

【This is an attack where the application becomes unavailable for the purpose it was designed due to the way it processes incoming HTTP requests. These requests need not be deliberately crafted by a malicious actor: a misconfigured or buggy client can also send a pattern of requests to the server that result in a denial of service.】

HTTP 请求由 Node.js HTTP 服务器接收,并通过注册的请求处理程序交给应用代码。服务器不会解析请求体的内容。因此,在请求体内容交给请求处理程序之后引起的任何 DoS(拒绝服务)问题,并不是 Node.js 本身的漏洞,因为正确处理它是应用代码的责任。

【HTTP requests are received by the Node.js HTTP server and handed over to the application code via the registered request handler. The server does not parse the content of the request body. Therefore any DoS caused by the contents of the body after they are handed over to the request handler is not a vulnerability in Node.js itself, since it's the responsibility of the application code to handle it correctly.】

确保 WebServer 正确处理套接字错误,例如,当服务器在没有错误处理程序的情况下创建时,它将容易受到拒绝服务攻击(DoS)的影响

【Ensure that the WebServer handles socket errors properly, for instance, when a server is created without an error handler, it will be vulnerable to DoS】

const  = ('node:net');

const  = .(function () {
  // socket.on('error', console.error) // this prevents the server to crash
  .('Echo server\r\n');
  .();
});

.(5000, '0.0.0.0');

如果执行了 _ 错误请求 _,服务器可能会崩溃。

【If a bad request is performed the server could crash.】

一个不由请求内容引起的 DoS 攻击示例是 Slowloris。在这种攻击中,HTTP 请求会被缓慢且分段地发送,每次发送一个片段。在完整请求送达之前,服务器会一直为正在进行的请求分配资源。如果同时发送足够多的此类请求,并发连接数量很快就会达到最大值,从而导致拒绝服务。这就是该攻击不依赖于请求内容,而是依赖于发送到服务器的请求的时间和模式的原因。

【An example of a DoS attack that is not caused by the request's contents is Slowloris. In this attack, HTTP requests are sent slowly and fragmented, one fragment at a time. Until the full request is delivered, the server will keep resources dedicated to the ongoing request. If enough of these requests are sent at the same time, the amount of concurrent connections will soon reach its maximum resulting in a denial of service. This is how the attack depends not on the request's contents but on the timing and pattern of the requests being sent to the server.】

缓解措施

  • 使用反向代理来接收并转发请求到 Node.js 应用。反向代理可以提供缓存、负载均衡、IP 黑名单等功能,从而减少拒绝服务(DoS)攻击有效的可能性。
  • 正确配置服务器超时,以便可以断开空闲连接或请求到达过慢的连接。请参阅 http.Server 中的不同超时设置,尤其是 headersTimeoutrequestTimeouttimeoutkeepAliveTimeout
  • 限制每个主机和总共打开的套接字数量。请参阅 http 文档,特别是 agent.maxSocketsagent.maxTotalSocketsagent.maxFreeSocketsserver.maxRequestsPerSocket

DNS 重绑定 (CWE-346)

【DNS Rebinding (CWE-346)】

这是一次可以针对启用了调试检查器的 Node.js 应用的攻击,利用 --inspect 开关

【This is an attack that can target Node.js applications being run with the debugging inspector enabled using the --inspect switch.】

由于在网页浏览器中打开的网站可以发起 WebSocket 和 HTTP 请求,它们可以针对本地运行的调试检查器。现代浏览器通常通过实现 同源策略 来防止这种情况,同源策略 禁止脚本访问来自不同源的资源(意味着恶意网站无法读取从本地 IP 地址请求的数据)。

【Since websites opened in a web browser can make WebSocket and HTTP requests, they can target the debugging inspector running locally. This is usually prevented by the same-origin policy implemented by modern browsers, which forbids scripts from reaching resources from different origins (meaning a malicious website cannot read data requested from a local IP address).】

然而,通过 DNS 重新绑定,攻击者可以暂时控制其请求的源,使其看起来像是来自本地 IP 地址。这是通过控制网站和用于解析其 IP 地址的 DNS 服务器来完成的。更多详情请参见 DNS 重新绑定维基

【However, through DNS rebinding, an attacker can temporarily control the origin for their requests so that they seem to originate from a local IP address. This is done by controlling both a website and the DNS server used to resolve its IP address. See DNS Rebinding wiki for more details.】

缓解措施

  • 通过向 SIGUSR1 信号附加 process.on('SIGUSR1', …) 监听器来禁用检查器。
  • 不要在生产环境中运行检查器协议。

敏感信息暴露给未授权的行为者 (CWE-552)

【Exposure of Sensitive Information to an Unauthorized Actor (CWE-552)】

当前目录中包含的所有文件和文件夹在发布包时都会被推送到 npm 注册表。

【All the files and folders included in the current directory are pushed to the npm registry during the package publication.】

有一些机制可以通过使用 .npmignore.gitignore 定义阻止列表,或者通过在 package.json 中定义允许列表来控制这种行为

【There are some mechanisms to control this behavior by defining a blocklist with .npmignore and .gitignore or by defining an allowlist in the package.json

缓解措施

  • 使用 npm publish --dry-run 列出所有要发布的文件。在发布包之前,请确保检查内容。
  • 创建和维护忽略文件(例如 .gitignore.npmignore)也很重要。在这些文件中,你可以指定哪些文件/文件夹不应该被发布。package.json 中的 文件属性 允许执行相反的操作——允许列表。
  • 如果发生接触,请确保 取消发布该软件包

HTTP 请求走私(CWE-444)

【HTTP Request Smuggling (CWE-444)】

这是一种涉及两个 HTTP 服务器(通常是代理和 Node.js 应用)的攻击。客户端发送一个 HTTP 请求,该请求首先通过前端服务器(代理),然后被重定向到后端服务器(应用)。当前端和后端对模棱两可的 HTTP 请求解释不同的时候,攻击者有可能发送一个恶意消息,这条消息前端看不到,但后端可以看到,从而有效地“走私”它绕过代理服务器。

【This is an attack that involves two HTTP servers (usually a proxy and a Node.js application). A client sends an HTTP request that goes first through the front-end server (the proxy) and then is redirected to the back-end server (the application). When the front-end and back-end interpret ambiguous HTTP requests differently, there is potential for an attacker to send a malicious message that won't be seen by the front-end but will be seen by the back-end, effectively "smuggling" it past the proxy server.】

请参阅 CWE-444 以获取更详细的描述和示例。

【See the CWE-444 for a more detailed description and examples.】

由于此攻击依赖于 Node.js 对 HTTP 请求的解释方式与任意 HTTP 服务器不同,因此一次成功的攻击可能是由于 Node.js、前端服务器或两者的漏洞造成的。如果 Node.js 对请求的解释方式符合 HTTP 规范(见 RFC7230),那么这不被认为是 Node.js 的漏洞。

【Since this attack depends on Node.js interpreting HTTP requests differently from an (arbitrary) HTTP server, a successful attack can be due to a vulnerability in Node.js, the front-end server, or both. If the way the request is interpreted by Node.js is consistent with the HTTP specification (see RFC7230), then it is not considered a vulnerability in Node.js.】

缓解措施

  • 创建 HTTP 服务器时不要使用 insecureHTTPParser 选项。
  • 配置前端服务器以规范模糊请求。
  • 持续监控 Node.js 及所选前端服务器上的新的 HTTP 请求走私漏洞。
  • 尽可能使用端到端的 HTTP/2,并禁用 HTTP 降级。

通过时间攻击泄露信息 (CWE-208)

【Information Exposure through Timing Attacks (CWE-208)】

这是一种攻击,攻击者可以通过例如测量应用响应请求所需的时间来获取潜在的敏感信息。这种攻击并非特定于 Node.js,几乎可以针对所有运行时环境。

【This is an attack that allows the attacker to learn potentially sensitive information by, for example, measuring how long it takes for the application to respond to a request. This attack is not specific to Node.js and can target almost all runtimes.】

只要应用在时间敏感操作(例如分支)中使用秘密,攻击就有可能发生。考虑在典型应用中处理身份验证。在这里,基本的身份验证方法包括将电子邮件和密码作为凭据。用户信息是从用户提供的输入中检索的,理想情况下来自数据库管理系统(DBMS)。在检索到用户信息后,密码会与从数据库中检索到的用户信息进行比较。使用内置的字符串比较会使相同长度的值耗费更长时间。此比较在执行一段可接受的时间时,会无意中增加请求的响应时间。通过比较请求的响应时间,攻击者可以在大量请求中猜测密码的长度和值。

【The attack is possible whenever the application uses a secret in a timing-sensitive operation (e.g., branch). Consider handling authentication in a typical application. Here, a basic authentication method includes email and password as credentials. User information is retrieved from the input the user has supplied from ideally a DBMS. Upon retrieving user information, the password is compared with the user information retrieved from the database. Using the built-in string comparison takes a longer time for the same-length values. This comparison, when run for an acceptable amount unwillingly increases the response time of the request. By comparing the request response times, an attacker can guess the length and the value of the password in a large quantity of requests.】

缓解措施

  • 加密 API 提供了一个函数 timingSafeEqual,用于使用常量时间算法比较实际值和预期的敏感值。
  • 对于密码比较,你也可以使用原生加密模块中提供的 scrypt
  • 更一般地说,避免在变量时间操作中使用秘密信息。这包括根据秘密信息进行分支操作,以及当攻击者可能与你在同一基础设施上(例如同一云主机)时,将秘密信息用作内存索引。在 JavaScript 中编写常量时间代码很难(部分原因是 JIT)。对于加密应用,使用内置的加密 API 或 WebAssembly(用于本地未实现的算法)。

恶意第三方模块 (CWE-1357)

【Malicious Third-Party Modules (CWE-1357)】

根据 Node.js 威胁模型,要求使用恶意第三方模块的场景被视为 Node.js 核心的漏洞,因为 Node.js 将其被要求运行的代码(包括依赖)视为可信。然而,恶意或被破解的依赖仍然是 Node.js 用户最关键的 应用层 风险之一,应予以相应对待。

【According to the Node.js threat model, scenarios that require a malicious third-party module are not considered vulnerabilities in Node.js core, because Node.js treats the code it is asked to run (including dependencies) as trusted. However, malicious or compromised dependencies remain one of the most critical application-level risks for Node.js users and should be treated as such.】

目前,在 Node.js 中,任何包都可以访问强大的资源,例如网络访问。此外,因为它们也可以访问文件系统,所以可以将任何数据发送到任何地方。

【Currently, in Node.js, any package can access powerful resources such as network access. Furthermore, because they also have access to the file system, they can send any data anywhere.】

运行在 Node 进程中的所有代码都有能力通过使用 eval()(或其等效方法)加载和执行额外的任意代码。所有具有文件系统写入权限的代码也可以通过写入新文件或已有文件来实现相同的效果,这些文件会被加载执行。

【All code running into a node process has the ability to load and run additional arbitrary code by using eval()(or its equivalents). All code with file system write access may achieve the same thing by writing to new or existing files that are loaded.】

示例

  • 攻击者入侵了一个流行日志库的维护者账户,并发布了一个新的小版本,该版本在初始化日志器时会将环境变量(例如,数据库密码或访问令牌)传输到远程服务器。
  • 一个与知名框架名字相似的拼写劫持包被发布到 npm 注册表中。安装后,它会运行一个 postinstall 脚本,将开发者机器上的 SSH 密钥发送到攻击者控制的端点。

确保固定依赖版本,并使用常用工作流或 npm 脚本运行自动漏洞检查。在安装软件包之前,确保该软件包得到维护,并包含你所期望的所有内容。请注意,GitHub 上的源代码并不总是与发布的版本相同,请在 node_modules 中进行验证。

【Be sure to pin dependency versions and run automatic checks for vulnerabilities using common workflows or npm scripts. Before installing a package make sure that this package is maintained and includes all the content you expected. Be careful, the GitHub source code is not always the same as the published one, validate it in the node_modules.】

供应链攻击

【Supply chain attacks】

对 Node.js 应用的供应链攻击发生在其某个依赖(直接依赖或传递依赖)被攻破时。这可能是由于应用对依赖的规范过于宽松(允许不必要的更新)和/或规范中常见的拼写错误(易受 typosquatting 攻击)所致。

【A supply chain attack on a Node.js application happens when one of its dependencies (either direct or transitive) are compromised. This can happen either due to the application being too lax on the specification of the dependencies (allowing for unwanted updates) and/or common typos in the specification (vulnerable to typosquatting).】

攻击者如果控制了上游包,就可以发布包含恶意代码的新版本。如果 Node.js 应用依赖该包而没有严格限制可使用的安全版本,该包可能会被自动更新到最新的恶意版本,从而危及应用安全。

【An attacker who takes control of an upstream package can publish a new version with malicious code in it. If a Node.js application depends on that package without being strict on which version is safe to use, the package can be automatically updated to the latest malicious version, compromising the application.】

package.json 文件中指定的依赖可以使用确切的版本号或版本范围。然而,当将依赖固定为确切版本时,其传递依赖本身并不会被固定。这仍然使应用容易受到不必要或意外更新的影响。

【Dependencies specified in the package.json file can have an exact version number or a range. However, when pinning a dependency to an exact version, its transitive dependencies are not themselves pinned. This still leaves the application vulnerable to unwanted/unexpected updates.】

可能的攻击向量:

【Possible attack vectors:】

  • 域名拼写劫持攻击
  • 锁文件中毒
  • 受影响的维护者
  • 恶意软件包
  • 依赖混淆

缓解措施

  • 使用 --ignore-scripts 防止 npm 执行任意脚本
    • 此外,你可以通过 npm config set ignore-scripts true 全局禁用它
  • 将依赖版本固定为特定的不可变版本,而不是版本范围或可变来源的版本。
  • 使用锁定文件,它可以固定每个依赖(直接和间接的)。
  • 使用 CI 自动检查新漏洞,使用 npm-audit 等工具。
    • 可以使用诸如 Socket 的工具通过静态分析来分析软件包,以发现诸如网络或文件系统访问等风险行为。
  • 使用 npm ci 替代 npm install。这样可以强制执行锁定文件,如果锁定文件与 package.json 文件之间不一致,将会报错(而不是默默地忽略锁定文件而使用 package.json)。
  • 仔细检查 package.json 文件,查看依赖名称中是否有错误或拼写错误。

内存访问违规 (CWE-284)

【Memory Access Violation (CWE-284)】

基于内存或基于堆的攻击依赖于内存管理错误和可利用的内存分配器。像所有运行时一样,如果你的项目在共享机器上运行,Node.js 也容易受到这些攻击。使用安全堆有助于防止因指针溢出或下溢而导致的敏感信息泄露。

【Memory-based or heap-based attacks depend on a combination of memory management errors and an exploitable memory allocator. Like all runtimes, Node.js is vulnerable to these attacks if your projects run on a shared machine. Using a secure heap is useful for preventing sensitive information from leaking due to pointer overruns and underruns.】

不幸的是,Windows 上无法使用安全堆。更多信息可以在 Node.js 安全堆文档 上找到。

【Unfortunately, a secure heap is not available on Windows. More information can be found on Node.js secure-heap documentation.】

缓解措施

  • 根据你的应用使用 --secure-heap=n,其中 n 是分配的最大字节数。
  • 不要在共享机器上运行你的生产应用。

猴子补丁(CWE-349)

【Monkey Patching (CWE-349)】

Monkey patching 指的是在运行时修改属性以改变现有行为。例如:

【Monkey patching refers to the modification of properties in runtime aiming to change the existing behavior. Example:】

.. = function () {
  // overriding the global [].push
};

缓解措施

--frozen-intrinsics 标志启用实验性的冻结内置对象,这意味着所有内置的 JavaScript 对象和函数都会被递归冻结。因此,以下代码片段 不会 覆盖 Array.prototype.push 的默认行为。

【The --frozen-intrinsics flag enables experimental¹ frozen intrinsics, which means all the built-in JavaScript objects and functions are recursively frozen. Therefore, the following snippet will not override the default behavior of Array.prototype.push

.. = function () {
  // overriding the global [].push
};

// Uncaught:
// TypeError <Object <Object <[Object: null prototype] {}>>>:
// Cannot assign to read only property 'push' of object ''

然而,重要的是要提到,你仍然可以使用 globalThis 定义新的全局变量并替换现有的全局变量

【However, it’s important to mention you can still define new globals and replace existing globals using globalThis

> globalThis.foo = 3; foo; // you can still define new globals
3
> globalThis.Array = 4; Array; // However, you can also replace existing globals
4

因此,可以使用 Object.freeze(globalThis) 来保证不会替换任何全局变量。

【Therefore, Object.freeze(globalThis) can be used to guarantee no globals will be replaced.】

原型污染攻击 (CWE-1321)

【Prototype Pollution Attacks (CWE-1321)】

根据 Node.js 威胁模型,依赖于攻击者控制的用户输入的原型污染 被视为 Node.js 核心的漏洞,因为 Node.js 信任应用代码提供的输入。尽管如此,原型污染对于 Node.js 应用和第三方库来说仍然是一类严重的漏洞,你应在应用和依赖层面实现防护措施。

【Per the Node.js threat model, prototype pollution that relies on an attacker controlling user input is not considered a vulnerability in Node.js core, because Node.js trusts the inputs provided by application code. Nonetheless, prototype pollution is a serious class of vulnerabilities for Node.js applications and third-party libraries, and you should implement defenses at the application and dependency level.】

原型污染是指通过滥用 protoconstructorprototype 以及其他继承自内置原型的属性,有可能修改或注入 JavaScript 语言元素的属性。

【Prototype pollution refers to the possibility of modifying or injecting properties into Javascript language items by abusing the usage of __proto_, _constructor, prototype, and other properties inherited from built-in prototypes.】

const  = { : 1, : 2 };
const  = .('{"__proto__": { "polluted": true}}');

const  = .({}, , );
.(.polluted); // true

// Potential DoS
const  = .('{"__proto__": null}');
const  = .(, );
.hasOwnProperty('b'); // Uncaught TypeError: d.hasOwnProperty is not a function

这是继承自 JavaScript 语言的潜在漏洞。

【This is a potential vulnerability inherited from the JavaScript language.】

例子

其他场景包括:

【Additional scenarios include:】

  • 一个 Web API 在没有验证的情况下将不可信的 JSON 请求体合并到共享配置对象中。通过发送带有 __proto__ 属性的负载,攻击者可以在此过程中向许多对象添加意外属性,从而导致逻辑错误或拒绝服务。
  • 一个模板渲染服务接受用户控制的选项,并将它们直接传递给深度合并工具。通过污染 Object.prototype,攻击者可以使所有未来的模板行为异常,可能绕过依赖对象属性存在的安全检查。

缓解措施

  • 避免 不安全的递归合并,参见 CVE-2018-16487
  • 针对外部/不可信请求实现 JSON 模式验证。
  • 通过使用 Object.create(null) 来创建没有原型的对象。
  • 冻结原型:Object.freeze(MyObject.prototype)
  • 使用 --disable-proto 标志禁用 Object.prototype.__proto__ 属性。
  • 使用 Object.hasOwn(obj, keyFromObj) 检查属性是否直接存在于对象上,而不是来自原型。
  • 避免使用 Object.prototype 中的方法。

不受控的搜索路径元素 (CWE-427)

【Uncontrolled Search Path Element (CWE-427)】

Node.js 威胁模型 将环境中 Node.js 可访问的文件系统视为可信。因此,仅依赖于控制这些位置的文件的问题被视为 Node.js 核心的漏洞。然而,它们与整体部署和供应链的安全性相关,因此你应该加强环境安全,并使用以下机制来降低风险。

【The Node.js threat model considers the file system in the environment accessible to Node.js as trusted. As a result, issues that rely solely on controlling files in those locations are not considered vulnerabilities in Node.js core. They are, however, relevant to the security of your overall deployment and supply chain, so you should harden your environment and use the mechanisms below to reduce risk.】

Node.js 按照 模块解析算法 加载模块。因此,它假设请求(require)模块的目录是可信的。

【Node.js loads modules following the Module Resolution Algorithm. Therefore, it assumes the directory in which a module is requested (require) is trusted.】

由此,这意味着预期的应用行为如下。假设以下目录结构:

【By that, it means the following application behavior is expected. Assuming the following directory structure:】

  • app/
    • server.js
    • auth.js
    • 认证

如果 server.js 使用 require('./auth'),它将遵循模块解析算法并加载 auth 而不是 auth.js

【If server.js uses require('./auth') it will follow the module resolution algorithm and load auth instead of auth.js.】

Node.js 权限模型

【Node.js Permission Model】

Node.js 提供了一个 权限模型,可以用于限制特定进程在运行时可以执行的操作。这个模型补充了 Node.js 威胁模型

【Node.js provides a permission model that can be used to restrict what a given process is allowed to do at runtime. This model complements the Node.js threat model.】

启用后(例如,使用 --permission 标志),权限模型允许你有选择地允许或拒绝对敏感功能的访问,例如:

【When enabled (for example, using the --permission flag), the permission model lets you selectively allow or deny access to sensitive capabilities such as:】

  • 文件系统的读写。
  • 网络访问(入站和出站)。
  • 子进程创建。
  • 使用本地插件和其他强大的 API。

这可以帮助限制恶意或被破坏的依赖、不受信任的配置或你自己代码中的意外行为的影响,因为即使是受信任的代码也会被防止执行超出你明确授予权限的操作。

【This can help contain the impact of malicious or compromised dependencies, untrusted configuration, or unexpected behavior in your own code, since even trusted code will be prevented from performing actions outside the permissions you have explicitly granted.】

请参阅 Node.js 权限文档 以获取最新的标志和选项。

【Refer to the Node.js permissions documentation for up-to-date flags and options.】

正式环境中的实验功能

【Experimental Features in Production】

不建议在生产环境中使用实验性功能。实验性功能可能会在必要时发生重大变化,其功能也不够稳定。不过,我们非常欢迎你的反馈。

【The use of experimental features in production isn't recommended. Experimental features can suffer breaking changes if needed, and their functionality isn't securely stable. Although, feedback is highly appreciated.】

OpenSSF 工具

【OpenSSF Tools】

OpenSSF 正在领导几个非常有用的项目,特别是如果你计划发布 npm 包的话。这些项目包括:

【The OpenSSF is leading several initiatives that can be very useful, especially if you plan to publish an npm package. These initiatives include:】

  • OpenSSF 评分卡 记分卡通过一系列自动化安全风险检查来评估开源项目。你可以使用它主动评估代码库中的漏洞和依赖,并就是否接受这些漏洞做出明智的决策。
  • OpenSSF 最佳实践徽章计划 项目可以通过描述它们如何遵守每项最佳实践来自愿进行自我认证。这将生成一个可以添加到项目中的徽章。

你还可以通过 OpenJS 安全协作空间 与其他项目和安全专家进行合作。

【You can also collaborate with other projects and security experts through the OpenJS Security Collaboration Space.】