权限#

权限可用于控制 Node.js 进程可以访问哪些系统资源或进程可以使用这些资源执行哪些操作。权限还可以控制哪些模块可以被其他模块访问。

  • 基于模块的权限 控制在应用程序执行期间哪些文件或 URL 可供其他模块使用。例如,这可以用来控制哪些模块可以被第三方依赖项访问。

  • 基于进程的权限 控制 Node.js 进程对资源的访问。资源可以完全允许或拒绝,或者可以控制与资源相关的操作。例如,可以允许文件系统读取,同时拒绝写入。

如果您发现潜在的安全漏洞,请参阅我们的 安全策略

基于模块的权限#

策略#

稳定性:1 - 实验性

Node.js 包含对在加载代码时创建策略的实验性支持。

策略是一项安全功能,旨在确保加载代码的完整性。

虽然它不充当溯源机制来追踪代码的来源,但它可以作为抵御恶意代码执行的强大防御措施。与可能在代码加载后限制功能的基于运行时的模型不同,Node.js 策略侧重于防止恶意代码首先完全加载到应用程序中。

使用策略假定对策略文件采取安全措施,例如确保策略文件无法被 Node.js 应用程序覆盖,方法是使用文件权限。

最佳实践是确保策略清单对正在运行的 Node.js 应用程序是只读的,并且正在运行的 Node.js 应用程序无法以任何方式更改该文件。典型的设置是将策略文件创建为与运行 Node.js 的用户 ID 不同的用户 ID,并向运行 Node.js 的用户 ID 授予读取权限。

启用#

--experimental-policy 标志可用于在加载模块时启用策略功能。

设置完此标志后,所有模块都必须符合传递给该标志的策略清单文件。

node --experimental-policy=policy.json app.js 

策略清单将用于对 Node.js 加载的代码实施约束。

为了防止磁盘上的策略文件被篡改,可以通过 `--policy-integrity` 提供策略文件本身的完整性校验。这样即使磁盘上的文件被修改,也可以运行 `node` 并断言策略文件的内容。

node --experimental-policy=policy.json --policy-integrity="sha384-SggXRQHwCG8g+DktYYzxkXRIkTiEYWBHqev0xnpCxYlqMBufKZHAHQM3/boDaI/0" app.js 
功能#
错误行为#

当策略检查失败时,Node.js 默认会抛出错误。可以通过在策略清单中定义一个 "onerror" 字段来更改错误行为,可以将其更改为以下几种可能性之一。

  • "exit": 将立即退出进程。不允许运行任何清理代码。
  • "log": 将在失败处记录错误。
  • "throw": 将在失败处抛出 JS 错误。这是默认行为。
{
  "onerror": "log",
  "resources": {
    "./app/checked.js": {
      "integrity": "sha384-SggXRQHwCG8g+DktYYzxkXRIkTiEYWBHqev0xnpCxYlqMBufKZHAHQM3/boDaI/0"
    }
  }
} 
完整性检查#

策略文件必须使用与浏览器 integrity 属性 兼容的子资源完整性字符串进行完整性检查,该属性与绝对 URL 相关联。

当使用 `require()` 或 `import` 时,如果已指定策略清单,则会检查加载所涉及的所有资源的完整性。如果资源与清单中列出的完整性不匹配,则会抛出错误。

一个允许加载文件 `checked.js` 的示例策略文件

{
  "resources": {
    "./app/checked.js": {
      "integrity": "sha384-SggXRQHwCG8g+DktYYzxkXRIkTiEYWBHqev0xnpCxYlqMBufKZHAHQM3/boDaI/0"
    }
  }
} 

策略清单中列出的每个资源都可以使用以下格式之一来确定其位置

  1. 从清单到资源的 相对 URL 字符串,例如 `./resource.js`、`../resource.js` 或 `/resource.js`。
  2. 到资源的完整 URL 字符串,例如 `file:///resource.js`。

加载资源时,整个 URL 必须匹配,包括搜索参数和哈希片段。`./a.js?b` 在尝试加载 `./a.js` 时不会被使用,反之亦然。

要生成完整性字符串,可以使用类似 `node -e 'process.stdout.write("sha256-");process.stdin.pipe(crypto.createHash("sha256").setEncoding("base64")).pipe(process.stdout)' < FILE` 的脚本。

完整性可以指定为布尔值true,以接受资源的任何主体,这对于本地开发很有用。在生产环境中不建议使用,因为它会允许对资源进行意外的更改,并被视为有效。

依赖项重定向#

应用程序可能需要发布模块的修补版本,或者阻止模块允许所有模块访问所有其他模块。重定向可以通过拦截尝试加载希望被替换的模块来实现。

{
  "resources": {
    "./app/checked.js": {
      "dependencies": {
        "fs": true,
        "os": "./app/node_modules/alt-os",
        "http": { "import": true }
      }
    }
  }
} 

依赖项以请求的说明符字符串为键,其值为truenull、指向要解析的模块的字符串或条件对象。

说明符字符串不执行任何搜索,必须与提供给require()import的字符串完全匹配,除了规范化步骤。因此,如果策略使用多个不同的字符串指向同一个模块(例如,排除扩展名),则可能需要多个说明符。

说明符字符串在用于匹配之前会被规范化,但不会被解析,以便与导入映射保持一定程度的兼容性,例如,如果资源file:///C:/app/utils.js从位于file:///C:/app/policy.json的策略中获得了以下重定向

{
  "resources": {
    "file:///C:/app/utils.js": {
      "dependencies": {
        "./utils.js": "./utils-v2.js"
      }
    }
  }
} 

任何用于加载file:///C:/app/utils.js的说明符都会被拦截并重定向到file:///C:/app/utils-v2.js,无论使用绝对说明符还是相对说明符。但是,如果使用非绝对或相对 URL 字符串的说明符,则不会被拦截。因此,如果使用诸如import('#utils')之类的导入,则不会被拦截。

如果重定向的值为true,则将使用策略文件顶部的“dependencies”字段。如果策略文件顶部的该字段为true,则使用默认的节点搜索算法来查找模块。

如果重定向的值为字符串,则相对于清单解析该字符串,然后立即使用该字符串,而无需搜索。

任何尝试解析但未在依赖项中列出的规范字符串,根据策略将导致错误。

依赖项映射的布尔值 true 可以指定为允许模块加载任何规范符而无需重定向。这对于本地开发很有用,并且在生产中可能有一些有效的用途,但应谨慎使用,并在审核模块以确保其行为有效后才使用。

类似于 package.json 中的 "exports",依赖项也可以指定为包含条件的对象,这些条件会分支依赖项的加载方式。在前面的示例中,当 "import" 条件是加载它的部分时,允许使用 "http"

解析值的 null 值会导致解析失败。这可以用来确保显式阻止某些类型的动态访问。

解析的模块位置的未知值会导致失败,但不能保证向前兼容。

策略重定向的所有保证都在 保证 部分中指定。

示例:修补的依赖项#

重定向的依赖项可以提供适合应用程序的衰减或修改的功能。例如,通过包装原始函数来记录有关函数持续时间的日志数据

const original = require('fn');
module.exports = function fn(...args) {
  console.time();
  try {
    return new.target ?
      Reflect.construct(original, args) :
      Reflect.apply(original, this, args);
  } finally {
    console.timeEnd();
  }
}; 
范围#

使用清单的 "scopes" 字段为多个资源一次设置配置。"scopes" 字段通过匹配资源的段来工作。如果范围或资源包含 "cascade": true,则将在其包含的范围内搜索未知规范符。级联的包含范围是通过递归地减少资源 URL 来找到的,方法是删除 特殊方案 的段,保留尾随的 "/" 后缀,并删除查询和哈希片段。这最终会导致 URL 缩减为其来源。如果 URL 不是特殊的,则范围将由 URL 的来源定位。如果找不到来源的范围,或者在不透明来源的情况下,可以使用协议字符串作为范围。如果找不到 URL 协议的范围,将使用最终的空字符串 "" 范围。

注意,blob: URL 的来源来自其包含的路径,因此 "blob:https://nodejs.ac.cn" 的范围将无效,因为没有 URL 的来源可以是 blob:https://nodejs.ac.cn;以 blob:https://nodejs.ac.cn/ 开头的 URL 将使用 https://nodejs.ac.cn 作为其来源,因此使用 https: 作为其协议范围。对于不透明来源的 blob: URL,它们将使用 blob: 作为其协议范围,因为它们不采用来源。

示例#
{
  "scopes": {
    "file:///C:/app/": {},
    "file:": {},
    "": {}
  }
} 

假设一个文件位于 file:///C:/app/bin/main.js,以下范围将按顺序检查

  1. "file:///C:/app/bin/"

这决定了 "file:///C:/app/bin/" 内所有基于文件的资源的策略。这不在策略的 "scopes" 字段中,将被跳过。将此范围添加到策略中会导致它在 "file:///C:/app/" 范围之前使用。

  1. "file:///C:/app/"

这决定了 "file:///C:/app/" 内所有基于文件的资源的策略。这在策略的 "scopes" 字段中,它将决定 file:///C:/app/bin/main.js 资源的策略。如果范围具有 "cascade": true,则关于该资源的任何未满足的查询将委托给 file:///C:/app/bin/main.js 的下一个相关范围,即 "file:"

  1. "file:///C:/"

这决定了 "file:///C:/" 内所有基于文件的资源的策略。这不在策略的 "scopes" 字段中,将被跳过。它不会用于 file:///C:/app/bin/main.js,除非 "file:///C:/app/" 设置为级联或不在策略的 "scopes" 中。

  1. "file:///"

这决定了 localhost 上所有基于文件的资源的策略。这不在策略的 "scopes" 字段中,将被跳过。它不会用于 file:///C:/app/bin/main.js,除非 "file:///C:/" 设置为级联或不在策略的 "scopes" 中。

  1. "file:"

这决定了所有基于文件的资源的策略。它不会用于 file:///C:/app/bin/main.js,除非 "file:///" 设置为级联或不在策略的 "scopes" 中。

  1. ""

这决定了所有资源的策略。它不会用于 file:///C:/app/bin/main.js,除非 "file:" 设置为级联。

使用范围的完整性#

在范围内将完整性设置为 true 将为清单中未找到的任何资源设置完整性为 true

在范围内将完整性设置为 null 将为清单中未找到的任何资源设置完整性为匹配失败。

不包含完整性与将完整性设置为 null 相同。

如果显式设置了 "integrity",则将忽略完整性检查的 "cascade"

以下示例允许加载任何文件

{
  "scopes": {
    "file:": {
      "integrity": true
    }
  }
} 
使用范围进行依赖项重定向#

以下示例允许访问 ./app/ 内所有资源的 fs

{
  "resources": {
    "./app/checked.js": {
      "cascade": true,
      "integrity": true
    }
  },
  "scopes": {
    "./app/": {
      "dependencies": {
        "fs": true
      }
    }
  }
} 

以下示例允许访问所有 data: 资源的 fs

{
  "resources": {
    "data:text/javascript,import('node:fs');": {
      "cascade": true,
      "integrity": true
    }
  },
  "scopes": {
    "data:": {
      "dependencies": {
        "fs": true
      }
    }
  }
} 
示例:模拟导入映射#

给定一个导入映射

{
  "imports": {
    "react": "./app/node_modules/react/index.js"
  },
  "scopes": {
    "./ssr/": {
      "react": "./app/node_modules/server-side-react/index.js"
    }
  }
} 
{
  "dependencies": true,
  "scopes": {
    "": {
      "cascade": true,
      "dependencies": {
        "react": "./app/node_modules/react/index.js"
      }
    },
    "./ssr/": {
      "cascade": true,
      "dependencies": {
        "react": "./app/node_modules/server-side-react/index.js"
      }
    }
  }
} 

导入映射 假设默认情况下可以获取任何资源。这意味着策略顶层的 "dependencies" 应设置为 true。策略要求这是可选的,因为它允许应用程序的所有资源进行交叉链接,这对于许多场景来说没有意义。它们还假设任何给定的范围可以访问其允许的依赖项之上的任何范围;所有模拟导入映射的范围都必须设置 "cascade": true

导入映射仅为其“导入”提供一个顶层范围。因此,要模拟 "imports",请使用 "" 范围。要模拟 "scopes",请以类似于导入映射中 "scopes" 工作方式的方式使用 "scopes"

注意事项:策略不使用字符串匹配来查找范围。它们执行 URL 遍历。这意味着 blob:data: URL 可能在两个系统之间不完全互操作。例如,导入映射可以通过在 / 字符上对 URL 进行分区来部分匹配 data:blob: URL,而策略有意不能。对于 blob: URL,导入映射范围不采用 blob: URL 的来源。

此外,导入映射仅适用于 import,因此可能需要在所有依赖项映射中添加 "import" 条件。

保证#
  • 策略保证使用 require()import()new Module() 加载模块时的文件完整性。
  • 重定向不会阻止通过诸如直接访问 require.cache 之类的方式访问 API,这些方式允许访问已加载的模块。策略重定向仅影响 require()import 的说明符。
  • 在策略威胁模型中,模块完整性的批准意味着允许它们在加载后修改甚至绕过安全功能,因此需要进行环境/运行时加固。

基于进程的权限#

权限模型#

稳定性:1.1 - 积极开发

Node.js 权限模型是一种机制,用于在执行期间限制对特定资源的访问。该 API 存在于一个标志 --experimental-permission 后面,启用该标志后,将限制对所有可用权限的访问。

可用权限由 --experimental-permission 标志记录。

使用 --experimental-permission 启动 Node.js 时,将限制通过 fs 模块访问文件系统、生成进程、使用 node:worker_threads、原生插件以及启用运行时检查器。

$ node --experimental-permission index.js
node:internal/modules/cjs/loader:171
  const result = internalModuleStat(filename);
                 ^

Error: Access to this API has been restricted
    at stat (node:internal/modules/cjs/loader:171:18)
    at Module._findPath (node:internal/modules/cjs/loader:627:16)
    at resolveMainPath (node:internal/modules/run_main:19:25)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:24)
    at node:internal/main/run_main_module:23:47 {
  code: 'ERR_ACCESS_DENIED',
  permission: 'FileSystemRead',
  resource: '/home/user/index.js'
} 

可以使用 --allow-child-process--allow-worker 分别允许访问生成进程和创建工作线程。

要允许在使用权限模型时使用原生插件,请使用 --allow-addons 标志。

运行时 API#

通过 --experimental-permission 标志启用权限模型时,将在 process 对象中添加一个新的属性 permission。此属性包含一个函数

permission.has(scope[, reference])#

API 调用以在运行时检查权限 (permission.has())

process.permission.has('fs.write'); // true
process.permission.has('fs.write', '/home/rafaelgss/protected-folder'); // true

process.permission.has('fs.read'); // true
process.permission.has('fs.read', '/home/rafaelgss/protected-folder'); // false 
文件系统权限#

要允许访问文件系统,请使用 --allow-fs-read--allow-fs-write 标志。

$ node --experimental-permission --allow-fs-read=* --allow-fs-write=* index.js
Hello world!
(node:19836) ExperimentalWarning: Permission is an experimental feature
(Use `node --trace-warnings ...` to show where the warning was created) 

这两个标志的有效参数是

  • * - 分别允许所有 FileSystemReadFileSystemWrite 操作。
  • 以逗号 (,) 分隔的路径,分别只允许匹配的 FileSystemReadFileSystemWrite 操作。

示例

  • --allow-fs-read=* - 它将允许所有 FileSystemRead 操作。
  • --allow-fs-write=* - 它将允许所有 FileSystemWrite 操作。
  • --allow-fs-write=/tmp/ - 它将允许 FileSystemWrite 访问 /tmp/ 文件夹。
  • --allow-fs-read=/tmp/ --allow-fs-read=/home/.gitignore - 它允许 FileSystemRead 访问 /tmp/ 文件夹 **以及** /home/.gitignore 路径。

也支持通配符

  • --allow-fs-read=/home/test* 将允许读取与通配符匹配的所有内容。例如:/home/test/file1/home/test2

在传递通配符字符 (*) 后,所有后续字符将被忽略。例如:/home/*.js 的作用类似于 /home/*

权限模型约束#

在使用此系统之前,您需要了解一些约束

  • 该模型不会继承到子节点进程或工作线程。
  • 使用权限模型时,以下功能将受到限制
    • 原生模块
    • 子进程
    • 工作线程
    • 检查器协议
    • 文件系统访问
  • 权限模型在设置 Node.js 环境后初始化。但是,某些标志(例如 --env-file--openssl-config)旨在在环境初始化之前读取文件。因此,此类标志不受权限模型规则的约束。
  • 在启用权限模型时,无法在运行时请求 OpenSSL 引擎,这会影响内置的 crypto、https 和 tls 模块。
限制和已知问题#
  • 启用权限模型时,Node.js 可能以与禁用权限模型时不同的方式解析某些路径。
  • 通过 CLI (--allow-fs-*) 不支持相对路径。
  • 符号链接将被跟踪,即使跟踪到已授予访问权限的路径集之外的位置。相对符号链接可能允许访问任意文件和目录。在启用权限模型启动应用程序时,您必须确保已授予访问权限的任何路径都不包含相对符号链接。