Node.js v20.7.0 文档


目录

权限#

权限可用于控制 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"
    }
  }
} 

完整性检查#

策略文件必须使用与与绝对 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 时不会使用 ./a.js?b,反之亦然。

要生成完整性字符串,则可以使用 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/server.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.cn" 的范围将无效,因为没有 URL 可以具有 blob:https://nodejs.cn 的来源; 以 blob:https://nodejs.cn/ 开头的 URL 将使用 https://nodejs.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:///" 设置为级联或不在策略的 "scopes" 中,否则它不会用于 file:///C:/app/bin/main.js

  1. "file:///"

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

  1. "file:"

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

  1. ""

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

使用范围的完整性#

在范围上将完整性设置为 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
      }
    }
  }
} 

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

{
  "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" 只有一个顶层范围。 所以为了模拟 "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 - 实验

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'
} 

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

运行时 API#

通过 --experimental-permission 标志启用权限模型时,新属性 permission 将添加到 process 对象。 此属性包含一个功能:

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/ 文件夹 and/home/.gitignore 路径。

也支持通配符:

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

限制和已知问题#

在使用此系统之前,你需要了解一些限制条件:

  • 启用权限模型时,Node.js 解析某些路径的方式可能与禁用时不同。
  • 使用权限模型时,原生模块默认受到限制。
  • 目前,启用权限模型后,无法在运行时请求 OpenSSL 引擎,从而影响内置的 crypto、https 和 tls 模块。
  • CLI (--allow-fs-*) 不支持相对路径。
  • 该模型不会继承到子节点进程。
  • 该模型不会继承到工作线程。
  • 创建符号链接时,目标(第一个参数)应该具有读写权限。
  • 权限更改不会追溯应用于现有资源。