likes
comments
collection
share

解读 Node.js 20 推出的权限模型

作者站长头像
站长
· 阅读数 17

大家好,我是 ConardLi

4月18号,Node.js 发布了 V20 版本,当前处于 Current 状态,预计会在今年 10 月份进入长期支持状态。

解读 Node.js 20 推出的权限模型

Node.js 20 算得上是一个相当大的版本了,其中带来了非常多的新特性,我先简单说说这些特性都是用来干啥的:

  • 新的权限模型:提供了 Node.js 中敏感 API 的权限管控能力

  • ESM Loader:可以在一个与主线程隔离的专用线程上自定义 ESM Loader

  • import.meta.resolve:可以将特定上下文的元数据共享给 JavaScript 模块

  • url.parse():可以识别端口号不是数字的 URL,比如 https://conardli.top:abc(ULR 的端口号一定是数字,这里其实是修了个 Bug)

  • url.parse():一个新的 URL 验证方法,在我这篇文章中有介绍过了:Node.js 支持原生 URL 验证方法,Node.js 14 即将停止维护!

  • test_runnerNode.js 提供了原生单元测试工具,已经稳定可用

  • V8引擎更新到 11.3 版本:支持了下面的方法:

    • String.prototype.isWellFormed:检测字符串格式是否正确
    • reverse、toReversed 等非破坏性数组方法,在我之前的文章中有详细介绍过:ECMAScript 2023 有哪些新东西?
    • ArrayBuffer、SharedArrayBuffer 可以调整容量大小

基本上主要的更新就是以上这些内容,其他的都没啥好展开讲的,今天我们主要一起来看看权限模型。

为什么要有权限模型?

首先明确一点,Node.js 20 的权限模型基本上是从 Deno 那抄过来的(目前看来只抄了个半成品)。

解读 Node.js 20 推出的权限模型

因为 Deno 在设计之初就考虑了权限设计,所以它可以很自豪的说:Deno is secure by default.

但是 Node.js 就不一样了,因为在一开始的设计中对安全考虑不足,也没有类似的权限模型的设计,所以频繁的被爆出各种漏洞,Node.js 官方还有一个专门的安全相关的文档,来告诉大家使用 Node.js 有什么样的风险:

解读 Node.js 20 推出的权限模型

而这些内容是需要开发者自己去规避的(但实际上大部分开发者不会主动关注,所以写出的很多代码都是有漏洞的)。

举个例子,Node.js 提供了很多操作文件的能力,比如下面这些 API

  • fs: createReadStream / createWriteStream / appendFileSync / appendFile...
  • fs-extra: createFile / createFileSync / ensureDir / ensureDirSync / mkdir...
  • shelljs: cat / cd / chmod / cp / pushd / popd / dirs / echo / find / grep...

假设我们把一个不可信的用户输入,传入到这些 API 的参数中,因为这意味着外部可以随意更改传入的参数,也就是说可以随意遍历我们的目录。

比如下面是一个实际的业务场景,我们在服务器的 './image' 这个相对路径下存储了一些图片,我们通过在参数中传入的图片名称来读取这张图片,代码如下:

function readImageg(ctx) {
  const { name } = ctx.query;
  const content = fs.readFileSync('./image' + name);
  return content;
}

看起来好像没什么问题,但是假如攻击者将 name 参数改为 ../../../../../../../etc/hosts 呢?

你可能会说,攻击者哪知道你的服务器有什么路径?

那攻击者可以随便传,随便试啊,也可以写脚本暴力测试,这就意味着任何人通过这个接口都可以读取你服务器上的任意文件。

就算是一个典型的路径遍历漏洞。

据我了解,有相当一部分 Node.js 开发者在写到类似的功能的时候会犯这种问题,因为他根本不会意识到有什么样的安全风险。

其实,这个问题的本质原因就是 Node.js 提供了很多高风险的 API,但是又没有给这些 API 比较严格的限制。所以 Node.js 不能说是默认安全的。

但是 Deno 可以说 Deno is secure by default. ,主要就是因为他提供了一套完整的权限模型。

上面提到了那么多可以读取文件的 API ,那么你在程序启动的时候就要告诉我:

这个程序是不是拥有读取文件的能力?如果要读取的话能读取哪些文件?

如果你没有告诉我,那我默认就不让你执行,如果你执行的路径没在我的白名单之内,那也不允许执行,这就是 secure by default

整个权限模型的核心思维就是这样的,下面我们来具体看看。

下面会有一些 Demo ,大家也可以自己在本地试试,首先要通过 nvm 升级到 20 版本:

nvm install 20.0.0
Downloading and installing node v20.0.0...
Downloading https://nodejs.org/dist/v20.0.0/node-v20.0.0-darwin-x64.tar.xz...
################################################################################################################################################# 100.0%
Computing checksum with shasum -a 256
Checksums matched!
Now using node v20.0.0 (npm v9.6.4)

文件系统的权限

上面我们提到 fs 模块为我们提供了很多和文件系统交互的 API,在没有启用权限标志的情况下,我们可以随便读取文件,首先我们创建一个 file17.js 来测试下:

const fs = require('node:fs/promises');

async function readFile() {
  try {
    const data = await fs.readFile('/Users/bytedance/Desktop/learn/Node.js/17.txt', {
      encoding: 'utf8',
    });
    console.log(data);
  } catch (err) {
    console.log(err);
  }
}

readFile();

我们执行 node file17.js ,它可以正常打印出内容:

node file17.js
171717171717

下面我们加上权限系统的标志 --experimental-permission ,在执行这个文件就会报错:

解读 Node.js 20 推出的权限模型

如果想要访问或文件系统,就必须要指定下面两个标志:

  • --allow-fs-read:允许读取某个路径下的文件,多个可以用逗号分隔;
  • --allow-fs-write:允许操作某个路径下的文件,多个可以用逗号分隔;

解读 Node.js 20 推出的权限模型

大家再回想一下上面提到了路径遍历漏洞的场景,如果我们的程序一开始设计的时候,启动就加上了 --experimental-permission ,那么默认就是不允许操作文件的,如果后面有了需要操作文件系统的需求,就要强制开发者通过 --allow-fs-read、--allow-fs-write 去设置白名单,那么我就可以说是默认安全的了。

检查权限

Node.js 20 还提供了一个 process.permission.has API,可以用来帮助我们在运行时检查程序是不是拥有指定的权限。我们使用刚刚的例子,在下面增加一些判断权限的代码:

const fs = require('node:fs/promises');

async function readFile() {
  try {
    const data = await fs.readFile('/Users/bytedance/Desktop/learn/Node.js/17.txt', {
      encoding: 'utf8',
    });
    console.log(data);
  } catch (err) {
    console.log(err);
  }
}

readFile();

console.log('通用文件读取权限', process.permission.has('fs.read'));

console.log(
  '/Users/bytedance/Desktop/learn/ 目录的读取权限',
  process.permission.has('fs.read', '/Users/bytedance/Desktop/learn/')
);

console.log('通用文件操作权限', process.permission.has('fs.write'));

console.log(
  '/Users/bytedance/Desktop/learn/ 目录的操作权限',
  process.permission.has('fs.write', '/Users/bytedance/Desktop/learn/')
);

我们执行一下这个命令:node --experimental-permission --allow-fs-read=/Users/bytedance/Desktop/learn/ file17.js

解读 Node.js 20 推出的权限模型

可以很清晰的告诉我们程序在运行时拥有什么权限。

子进程的权限

Node.js 中的 child_process 为我们提供了创建子进程的能力,我们可以在这个子进程中执行任意的 Shell 命令。

这个命令也是相当危险的,原理和上面的路径遍历一样,如果把用户可控的参数传入到了子进程执行的命令中,就是命令注入漏洞,这个要比路径遍历漏洞还严重的多。

我们创建一个 child17.js 文件来测试执行一个简单的 ls 命令:

const { spawn } = require('node:child_process');

const ls = spawn('ls', ['/Users/bytedance/Desktop/learn/Node.js']);

ls.stdout.on('data', (data) => {
  console.log(`输出如下:\n${data}`);
});

ls.stderr.on('data', (err) => {
  console.error(`失败: ${err}`);
});

ls.on('close', (code) => {
  console.log(`子进程推出 ${code}`);
});

执行结果如下,可以正常遍历目录:

解读 Node.js 20 推出的权限模型

但如果我们加上了 --experimental-permission 标志,执行命令的时候就会报错:

解读 Node.js 20 推出的权限模型

我们必须要加上 --allow-child-process 才能正常允许执行子进程。另外,如果子进程中读取到了一些文件目录,我们依然需要加上 --allow-fs-read 才能执行成功,下面我们执行这个命令:

node --experimental-permission --allow-child-process --allow-fs-read=/Users/bytedance/Desktop/learn/ child17.js

结果如下:

解读 Node.js 20 推出的权限模型

worker_threads 权限

worker_threads 可以让我们创建多线程,然后在不同的线程中并行执行代码,一般我们会开多个 Workers 来执行一些 CPU 密集型的操作。

我们创建一个 threads17.js 来测试执行下面的代码:

const { Worker, isMainThread, workerData } = require('node:worker_threads');

if (isMainThread) {

  new Worker(__filename, { workerData: 1});
  new Worker(__filename, { workerData: 2});

  console.log(`Inside Main Thread: isMainThread = ${isMainThread}`);

} else {

  console.log(`Inside Worker: isMainThread = ${isMainThread}, and my ID is ${workerData}`);
  
} 

代码里面创建了两个简单的 Worker 来并行执行代码,执行代码可以正常输出:

解读 Node.js 20 推出的权限模型

但如果我们启用了 --experimental-permission 标志,代码执行就会报错:

解读 Node.js 20 推出的权限模型

如果要使用 worker_threads ,比如要添加一个 --allow-worker 的标志,另外它还需要通过 --allow-fs-read 来告诉它哪些目录下的文件是允许被执行的,所以我们执行下面的代码:

 node --experimental-permission --allow-worker --allow-fs-read=/Users/bytedance/Desktop/learn/ threads17.js

这样就可以正常执行了:

解读 Node.js 20 推出的权限模型

最后

目前 Node.js 的权限模型还是相当初级的阶段,还有很多能力需要完善,比如可以限制服务的网络请求白名单,这样就可以避免 SSRF 漏洞等等,而且现在还在实验阶段,大家尽量不要在生产环境使用。

参考:

如果你想加入高质量前端交流群,或者你有任何其他事情想和我交流也可以添加我的个人微信 ConardLi