As Node.js continues to grow in popularity, security has become an increasingly important aspect of application development. With the release of Node version 20, a new experimental security feature has been introduced: the Permission Model. In this blog post, we will dive into the Permission Model, its capabilities, and how it can help you create more secure Node.js applications.
For a web service or background worker that mostly relies on network I/O, it makes little sense to let it run unrestricted as most tend to do. While this might be a already contained problem in production where it is (hopefully) running in its own container, it is a whole other issue when running on your local machine where any 3rd party library (that you are knowingly or unknowingly using) has access to your entire filesystem.
While this can and should already be solved by running node in a restricted user shell, most do not. And besides file system access we also want control over how and when child processes and worker threads are created, as well as any native addons our node code uses.
The Permission Model is an experimental mechanism that allows developers to restrict access to specific resources during execution. This feature aims to provide more control over various aspects of your Node.js applications, including:
- Restricting access to the file system (read and write)
- Restricting access to child_process
- Restricting access to worker_threads
- Restricting access to native addons
To start using the Permission Model, you will need to enable it with the --experimental-permission flag when running your Node.js application. This flag will restrict access to the file system, spawn processes, and use node:worker_threads by default.
node --experimental-permission index.js
The Permission Model comes with several flags that allow you to grant specific permissions for your application:
--allow-fs-write: Grant read and write access to the file system.
Example: Allow read and write access to the entire file system.
node --experimental-permission --allow-fs-read=* --allow-fs-write=* index.js
However, testing this on zsh and MacOS at the moment will just result in a:
zsh: no matches found: --allow-fs-read=*
Instead I had to use
--allow-fs-read-/ to make it function as intented.
Example: Allow write access to the /tmp/ folder and read access to the /home/index.js file.
node --experimental-permission --allow-fs-write=/tmp/ --allow-fs-read=/home/index.js index.js
node --experimental-permission --allow-child-process index.js
--allow-worker: Grant access to worker_threads.
$ node --experimental-permission --allow-worker index.js
When the Permission Model is enabled, you can use the new permission property of the process object to check if a certain permission has been granted at runtime:
process.permission.has('fs.write'); // true process.permission.has('fs.write', '/home/nodejs/protected-folder'); // true
With the introduction of the Permission Model in Node.js version 20, it's only natural to draw comparisons with Deno, which has built-in permissions from the get-go. In this section, we'll explore the similarities and differences between the Node.js Permission Model and Deno permissions, and how they impact security in application development.
As we've previously discussed, the Permission Model in Node.js is an experimental feature that grants developers granular control over access to resources such as the file system, child_process, worker_threads, and native addons. With various flags like --allow-fs-read, --allow-fs-write, --allow-child-process, and --allow-worker, developers can specify paths, use wildcard patterns, and check permissions at runtime.
--allow-read: Grant read access to the file system.
--allow-write: Grant write access to the file system.
--allow-net: Grant network access.
--allow-env: Grant access to environment variables.
--allow-plugin: Grant permission to load plugins.
--allow-hrtime: Grant permission to use high-resolution time measurement.
Maturity: The Node.js Permission Model is still an experimental feature, and its implementation may change in future releases. On the other hand, Deno permissions have been a part of the runtime since its inception and are more mature and stable.
Security Focus: Deno was designed with a strong focus on security, which is evident in its default permissions. By default, Deno scripts run in a sandbox without access to the file system, network, or environment variables. In contrast, Node.js has historically granted applications more access by default, and the Permission Model is an effort to mitigate potential security risks.
Permission Types: While both Node.js and Deno provide granular control over access to resources, Deno permissions are more extensive, covering areas such as network access, environment variables, and plugin loading. Node.js, on the other hand, currently focuses on the file system, child_process, worker_threads, and native addons.
Checking Permissions: Both Node.js and Deno allow developers to check permissions at runtime. In Node.js, you can use the process.permission property, while in Deno, you can use the Deno.permissions.query() method.
The Node.js Permission Model brings more control and security to your applications by allowing you to restrict access to specific resources during execution. While this feature is still experimental and may change in future releases, it shows the commitment of the Node.js community to create more secure applications.