GeezerVison is showing github.com — src,process: initial permission model implementation by RafaelGSS · Pull Request #44004 · nodejs/node
Conversation
RafaelGSS
commented
Jul 27, 2022
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
Reference issue: nodejs/security-wg#791
The tagged issue contains the initial proposal for this MVP. This Pull Request includes the foundation of the Permission Model.
Constraints
This Permission Model is not bulletproof, which means, there are constraints we agree on before landing this system:
- It’s not a sandbox, we assume the user trusts in the running code.
- No break-changes are ideal.
- It must add a low/no overhead when disabled and low overhead when enabled.
API Design
The Node.js Permission Model is a mechanism to restrict access to specific resources during the program execution. The API exists behind a flag --experimental-permission which when enabled, will restrict access to all available permissions.
Currently, the available permissions are:
- File System - manageable through
--allow-fs-read and --allow-fs-write flags
- Child Process - manageable through
--allow-child-process flag
- Worker Threads - manageable through
--allow-worker flag
Therefore, when starting a Node.js process with --experimental-permission,
the ability to access the filesystem, spawn process and,
create worker_threads will be restricted.
The CLI Arguments
To allow access to the filesystem, use the --allow-fs-read and
--allow-fs-write flags:
The valid arguments for both flags are:
* - To allow all operations to given scope (read/write).
- Paths delimited by comma (,) to manage reading/writing operations.
Example:
--allow-fs-read=/tmp/ - It will allow FileSystemRead access to the /tmp/ folder
--allow-fs-read=/tmp/,/home/.gitignore - It allows FileSystemRead access to the /tmp/ folder and the /home/.gitignore file — Relative paths are NOT supported.
Due to the PrefixRadixTree (fs_permission) lookup, relative paths are not supported. For this reason, the possiblyTransformPath was needed. I do have plans to create a pretty similar path.resolve on the C++ side so the possiblyTransformPath won't be needed, but I'll do it in a second iteration.
You can also mix both arguments:
--allow-fs-write=* --allow-fs-read=/tmp/ - It will allow FileSystemRead access to the /tmp/ folder and allow all the FileSystemWrite operations.
Note: It accepts wildcard parameters as well. For instance: --allow-fs-write=/home/test* will allow everything that matches the wildcard. e.g: /home/test/file1 / /home/test2
Note: I rather prefer reading those arguments from a file (policy-deny.json), instead passing them in the command line. However, to reduce the PR scope, I've decided to do it in a separate PR.
The API Arguments
A new property permission was added to the process module. The property contains two functions:
deny(scope [,parameters])
API Call to deny permissions at runtime. e.g (REMOVED)
process.permission.deny('fs') // deny permissions to ALL fs operations
process.permission.deny('fs.out') // deny permissions to ALL FileSystemWrite operations
process.permission.deny('fs.out', '/home/rafaelgss/protected-folder') // deny FileSystemWrite permissions to the protected-folder
process.permission.deny('fs.in') // deny permissions to ALL FileSystemRead operations
process.permission.deny('fs.in', '/home/rafaelgss/protected-folder') // deny FileSystemRead permissions to the protected-folder
API Call to check permissions at runtime. e.g:
process.permission.has('fs.out') // true
process.permission.has('fs.out', '/home/rafaelgss/protected-folder') // true
process.permission.deny('fs.out', '/home/rafaelgss/protected-folder')
process.permission.has('fs.out') // true
process.permission.has('fs.out', '/home/rafaelgss/protected-folder') // false
Future implementations
The implementation of the next features like “net” or “env” will be easily possible just by creating a new net_permission.h and implementing the PermissionBase methods.

FAQ
The user should be able to grant permissions in runtime?
No. Much like with other well-known and well-used permissions systems, code ought to be able to decide it can drop privileges, but never be able to grant itself any expanded privileges.
Can I deny permissions to just a specific module?
No. The permission system is process-scoped. You can use the [policy](https://nodejs.org/api/policy.html) to restrict module access.
What if I spawn a process, it will inherit the root permissions?
A process that has --experimental-permission will not be able to spawn a child process by default. If the user explicitly allows it to spawn a child process, then it will be the user's responsibility to pass along the correct arguments.
Benchmarks
This feature adds a very low overhead (if any), either enabled or disabled). Please, note I'm measuring only the feature usage without restricted files/resources (a better benchmark suite will be created in subsequent PRs). The principal behavior is that it doesn't add overhead to the main fs usage. For example, using the benchmark/fs/readfile.js
comparisson between main and this PR (permission model disabled)
➜ node-benchmark-compare compare_fs_permission.csv
confidence improvement accuracy (*) (**) (***)
fs/readfile.js concurrent=1 len=1024 encoding='' duration=5 -0.58 % ±0.78% ±1.05% ±1.38%
fs/readfile.js concurrent=1 len=1024 encoding='utf-8' duration=5 -0.24 % ±0.44% ±0.58% ±0.76%
fs/readfile.js concurrent=1 len=16777216 encoding='' duration=5 0.15 % ±0.96% ±1.28% ±1.67%
fs/readfile.js concurrent=1 len=16777216 encoding='utf-8' duration=5 0.76 % ±1.16% ±1.56% ±2.04%
fs/readfile.js concurrent=10 len=1024 encoding='' duration=5 0.32 % ±0.39% ±0.52% ±0.68%
fs/readfile.js concurrent=10 len=1024 encoding='utf-8' duration=5 * -0.73 % ±0.57% ±0.76% ±0.99%
fs/readfile.js concurrent=10 len=16777216 encoding='' duration=5 -0.71 % ±1.54% ±2.06% ±2.69%
fs/readfile.js concurrent=10 len=16777216 encoding='utf-8' duration=5 0.09 % ±0.69% ±0.91% ±1.19%
Be aware that when doing many comparisons the risk of a false-positive result increases.
In this case, there are 8 comparisons, you can thus expect the following amount of false-positive results:
0.40 false positives, when considering a 5% risk acceptance (*, **, ***),
0.08 false positives, when considering a 1% risk acceptance (**, ***),
0.01 false positives, when considering a 0.1% risk acceptance (***)
comparisson enabling permission model (this branch)
node on feat/permission-system [⇕$] took 40.4s
➜ ./node benchmark/fs/readfile.js
fs/readfile.js concurrent=1 len=1024 encoding="" duration=5: 24,192.15827315814
fs/readfile.js concurrent=10 len=1024 encoding="" duration=5: 95,917.90322639987
fs/readfile.js concurrent=1 len=16777216 encoding="" duration=5: 612.9672164291768
fs/readfile.js concurrent=10 len=16777216 encoding="" duration=5: 1,107.9227500862503
fs/readfile.js concurrent=1 len=1024 encoding="utf-8" duration=5: 24,314.915724686496
fs/readfile.js concurrent=10 len=1024 encoding="utf-8" duration=5: 92,569.37571752333
fs/readfile.js concurrent=1 len=16777216 encoding="utf-8" duration=5: 278.3922768971328
fs/readfile.js concurrent=10 len=16777216 encoding="utf-8" duration=5: 338.3870755499357
node on feat/permission-system [⇕$!] took 40.4s
➜ ./node benchmark/fs/readfile-permission-enabled.js
(node:62815) ExperimentalWarning: Permission is an experimental feature
(Use `node --trace-warnings ...` to show where the warning was created)
^C
node on feat/permission-system [⇕$!]
➜ ./node --no-warnings benchmark/fs/readfile-permission-enabled.js
fs/readfile-permission-enabled.js concurrent=1 len=1024 encoding="" duration=5: 24,185.367751785307
fs/readfile-permission-enabled.js concurrent=10 len=1024 encoding="" duration=5: 96,253.07607327335
fs/readfile-permission-enabled.js concurrent=1 len=16777216 encoding="" duration=5: 612.8715221066299
fs/readfile-permission-enabled.js concurrent=10 len=16777216 encoding="" duration=5: 1,106.1275272124165
fs/readfile-permission-enabled.js concurrent=1 len=1024 encoding="utf-8" duration=5: 24,375.648655236808
fs/readfile-permission-enabled.js concurrent=10 len=1024 encoding="utf-8" duration=5: 91,882.5214931248
fs/readfile-permission-enabled.js concurrent=1 len=16777216 encoding="utf-8" duration=5: 277.1205079822853
fs/readfile-permission-enabled.js concurrent=10 len=16777216 encoding="utf-8" duration=5: 337.30003270280775
Additional Considerations
cc: @nodejs/security-wg
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
BrunoBernardino, simonplend, bricss, micheleriva, rluvaton, matuzalemsteles, abdus, MomenNano, twlite, juliangruber, and 19 more reacted with hooray emoji
bricss, micheleriva, rluvaton, abdus, MomenNano, juliangruber, H4ad, ruyadorno, zombieleet, mcollina, and 15 more reacted with heart emoji
bricss, micheleriva, naugtur, rluvaton, MomenNano, outranker, marco-ippolito, ruyadorno, zombieleet, ovflowd, and 6 more reacted with rocket emoji
bricss, Barbapapazes, rluvaton, MomenNano, zombieleet, benjamingr, and jurosh reacted with eyes emoji
All reactions
-
🎉
29 reactions
-
❤️
25 reactions
-
🚀
16 reactions
-
👀
7 reactions
|
Review requested:
- @nodejs/gyp
- @nodejs/startup
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
benjamingr reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
If fs is completely denied, does it block the module loader? For example require('./package.json')
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
If fs is completely denied, does it block the module loader? For example require('./package.json')
Yes
rafaelgss@rafaelgss-desktop:~/repos/os/node$ ./node --policy-deny-fs=fs example.js
node:internal/modules/cjs/loader:157
const result = internalModuleStat(filename);
^
Error: Access to this API has been restricted
at stat (node:internal/modules/cjs/loader:157:18)
at Module._findPath (node:internal/modules/cjs/loader:531:16)
at resolveMainPath (node:internal/modules/run_main:19:25)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:71:24)
at node:internal/main/run_main_module:17:47 {
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemIn'
}
Node.js v19.0.0-pre
|
targos, addaleax, ruyadorno, and Qard reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
It must be able to restrict access to specific folders and files
Don't we actually want the opposite (in common use cases)?
It seems way more usual to me to need to deny access to most of the file system, with a few exceptions (allow list instead of deny list)
|
rluvaton, g-plane, metcoder95, and eugene1g reacted with thumbs up emoji
mcollina, joyeecheung, fardjad, and hansott reacted with heart emoji
All reactions
-
👍
4 reactions
-
❤️
4 reactions
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
It seems way more usual to me to need to deny access to most of the file system, with a few exceptions (allow list instead of deny list)
The problem is that it would block the Node.js internal functions. Node.js can't bypass those permissions since it runs the same code as the userland. If you deny access to the most of the file system, you would need to grant access to all internally needed FS operations manually.
I've talked to @jasnell and @addaleax previously and I don't think we're able to bypass Node.js needs without the complexity and compromising the security.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
I don't think we're able to bypass Node.js needs without the complexity and compromising the security.
To be fair, it’s probably quite a bit of work, but that doesn’t mean it isn’t worth doing. (This is something I’d not consider part of the initial implementation here, though.)
|
mcollina, jasnell, and Qard reacted with heart emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
It must be able to restrict access to specific folders and files
How is that going to work? For example, what prevents user code from creating a symlink from /tmp/xyz to $HOME/.ssh/id_rsa and then reading the protected file through said symlink?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Maybe we can allow changing the process policy only in the user land code and not in packages
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
legendecas reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
|
Given that @addaleax has already left a ton of comments, I'll wait a bit until those are resolved before going through and reviewing myself. Overall, however, on first read through things are looking pretty solid here. Happy to see this moving forward
|
RafaelGSS reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
How is that going to work? For example, what prevents user code from creating a symlink from /tmp/xyz to $HOME/.ssh/id_rsa and then reading the protected file through said symlink?
As mentioned in the description, this behavior wasn't addressed yet. I suggest discussing it in the next iteration (when all the code-review are fully resolved)
I assume you know about https://nodejs.org/api/policy.html ? Shouldn’t the configuration for this be part of that?
Yes. Honestly, this is quite different from the current policy behavior. We can discuss the nomenclature for sure, however, I suggest having this discussion later in this PR.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
How is that going to work? For example, what prevents user code from creating a symlink from /tmp/xyz to $HOME/.ssh/id_rsa and then reading the protected file through said symlink?
As mentioned in the description, this behavior wasn't addressed yet. I suggest discussing it in the next iteration (when all the code-review are fully resolved)
I think this is something that needs to be answered before this PR (or any PR with a path-based fs permissions model) can be merged, though.
|
tniessen reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
I think this is something that needs to be answered before this PR (or any PR with a path-based fs permissions model) can be merged, though.
Sure, I will. What I meant by "next iteration" is the next round of review after addressing the list mentioned in the PR description 😄
|
addaleax reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
I assume you know about nodejs.org/api/policy.html ? Shouldn’t the configuration for this be part of that?
Yes. Honestly, this is quite different from the current policy behavior. We can discuss the nomenclature for sure, however, I suggest having this discussion later in this PR.
I don’t think we can land a PR that adds a --policy-deny-* flag when we already have an existing --experimental-policy flag. Either this PR’s functionality needs to be configured through the policy.json file described in https://nodejs.org/api/policy.html or the existing policies functionality needs to be refactored to be defined via flags like --policy-deny-fs. We shouldn’t have two competing systems for security policies.
|
legendecas reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
RafaelGSS
commented
Jul 29, 2022
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
I don’t think we can land a PR that adds a --policy-deny-* flag when we already have an existing --experimental-policy flag. Either this PR’s functionality needs to be configured through the policy.json file described in https://nodejs.org/api/policy.html or the existing policies functionality needs to be refactored to be defined via flags like --policy-deny-fs. We shouldn’t have two competing systems for security policies.
That's indeed a good point. I'll mention it in the next Security WG. If you have any nomenclature suggestions, please raise them here 😄
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
That’s indeed a good point. I’ll mention it in the next Security WG. If you any nomenclature suggestion, please raise them here 😄
Looking in https://nodejs.org/api/policy.html, that feature is file/specifier specific, like “disable access to child_process“ or “ensure that app.js has this hash.” That’s too much data to try to convey via CLI flags, so I think we need to keep the separate file for defining all of that.
So I think maybe you could add a top-level section to that policy.json file for what you’re using flags for? Something like "permissions": { "deny": ["fs"] } or whatever you think is appropriate. And if what’s already in policies.json needs to move down a level under some new top-level field, that could happen too.
I agree there’s value in being able to define broad-brush permissions via a flag without needing a config file all the time (like if all the user wants to do is disable FS access or child processes or whatever, without getting into per-specifier definitions), so it’s probably worthwhile to keep the flag that this PR adds in addition to supporting the same via policies.json. We should probably come up with naming that makes sense when both exist, like --policy-file and --policy-permissions or something. I’ll leave the specifics to you and the Security WG; I just ask that you design the UX for this such that it makes sense alongside the policies feature that we already have. We should also probably create new flags as experimental at first, like --experimental-policy.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
So, all the errors are gone.
FWIW The CI was failing for two reasons:
- On node-test-commit-arm-debug: because
args.As<Array> behave slightly differently on ARM64 using Debug mode.
- On node-test-commit-custom-freestyle: because it uses the
tools/test.py --worker mode, which runs parallel tests inside a worker context, and when permission is enabled, workers aren't allowed unless --allow-worker is passed. To fix it, I had to skip the tests whenever permission is enabled and workers aren't allowed (See: tools/run-worker.js).
I'm planning to merge it soon (once test-asan finishes).
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
The CI is already green. I've pushed a performance improvement, once it's gets green again, I'll merge it
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
debadree25, RomainLanz, BrunoBernardino, RafaelGSS, and RaisinTen reacted with rocket emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Big time lurker on this PR, now that it will no longer be noisy, I want to share that I'm so excited about this!! This is the thing that excites me the most about Deno vs Node.js, so I'm very thrilled to see it being worked on for Node.js directly. Thank you for the amazing work!
|
RafaelGSS reacted with heart emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Feb 24, 2023
|
Would you mind rebasing against v18.x?
|
RafaelGSS reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Mar 2, 2023
RafaelGSS
commented
Mar 7, 2023
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
Given the following PRs/issues
we're going to have a baking-time of 1 release for this feature (after landing all the patches).
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
@RafaelGSS The blocked label isn't used by releasers to filter commits. I replaced it with backport-blocked
|
tniessen reacted with thumbs up emoji
RafaelGSS reacted with heart emoji
All reactions
-
👍
1 reaction
-
❤️
1 reaction
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
RafaelGSS
commented
Apr 3, 2023
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
Adding dont-land-on-v19.x. After discussing it with TSC we've agreed to release it straight in Node.js v20.
Note to Node.js users: we've decided to drop the process.permission.deny for now. See: #47335
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Labels
experimental
Issues and PRs related to experimental features.
lib / src
Issues and PRs involving general changes in the lib/ or src/ directories.
needs-ci
PRs that need a full CI run.
notable-change
PRs with changes that should be highlighted in changelogs.
permission
Issues and PRs related to the Permission Model.
security
Issues and PRs related to security.
semver-minor
PRs that contain new features and should be released in the next minor version.
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|