Skip to content

Commit e17f890

Browse files
committed
lib,permission: add permission.drop
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
1 parent 586403f commit e17f890

33 files changed

+879
-2
lines changed

doc/api/permissions.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ flag. For WASI, use the [`--allow-wasi`][] flag.
7676

7777
When enabling the Permission Model through the [`--permission`][]
7878
flag a new property `permission` is added to the `process` object.
79-
This property contains one function:
79+
This property contains the following functions:
8080

8181
##### `permission.has(scope[, reference])`
8282

@@ -90,6 +90,36 @@ process.permission.has('fs.read'); // true
9090
process.permission.has('fs.read', '/home/rafaelgss/protected-folder'); // false
9191
```
9292

93+
##### `permission.drop(scope[, reference])`
94+
95+
API call to drop permissions at runtime. This operation is **irreversible**.
96+
97+
When called without a reference, the entire scope is dropped. When called
98+
with a reference, only the permission for that specific resource is revoked.
99+
100+
You can only drop the exact resource that was explicitly granted. The
101+
reference passed to `drop()` must match the original grant. If a permission
102+
was granted using a wildcard (`*`), only the entire scope can be dropped
103+
(by calling `drop()` without a reference). If a directory was granted
104+
(e.g. `--allow-fs-read=/my/folder`), you cannot drop individual files
105+
inside it - you must drop the same directory that was originally granted.
106+
107+
```js
108+
const fs = require('node:fs');
109+
110+
// Read config at startup while we still have permission
111+
const config = fs.readFileSync('/etc/myapp/config.json', 'utf8');
112+
113+
// Drop read access to /etc/myapp after initialization
114+
process.permission.drop('fs.read', '/etc/myapp');
115+
116+
// This will now throw ERR_ACCESS_DENIED
117+
process.permission.has('fs.read', '/etc/myapp/config.json'); // false
118+
119+
// Drop child process permission entirely
120+
process.permission.drop('child');
121+
```
122+
93123
#### File System Permissions
94124

95125
The Permission Model, by default, restricts access to the file system through the `node:fs` module.

doc/api/process.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3197,6 +3197,65 @@ process.permission.has('fs.read', './README.md');
31973197
process.permission.has('fs.read');
31983198
```
31993199
3200+
### `process.permission.drop(scope[, reference])`
3201+
3202+
<!-- YAML
3203+
added: REPLACEME
3204+
-->
3205+
3206+
> Stability: 1.1 - Active Development
3207+
3208+
* `scope` {string}
3209+
* `reference` {string}
3210+
3211+
Drops the specified permission from the current process. This operation is
3212+
**irreversible** — once a permission is dropped, it cannot be restored through
3213+
any Node.js API.
3214+
3215+
If no reference is provided, the entire scope is dropped. For example,
3216+
`process.permission.drop('fs.read')` will revoke ALL file system read
3217+
permissions.
3218+
3219+
When a reference is provided, only the permission for that specific resource
3220+
is dropped. For example, `process.permission.drop('fs.read', '/etc/myapp')`
3221+
will revoke read access to that directory while keeping other read
3222+
permissions intact.
3223+
3224+
**Important:** You can only drop the exact resource that was explicitly
3225+
granted. The reference passed to `drop()` must match the original grant:
3226+
3227+
* If a permission was granted using a wildcard (`*`), such as
3228+
`--allow-fs-read=*`, individual paths cannot be dropped - only the entire
3229+
scope can be dropped (by calling `drop()` without a reference).
3230+
* If a directory was granted (e.g. `--allow-fs-read=/my/folder`), you cannot
3231+
drop access to individual files inside it. You must drop the same directory
3232+
that was granted. Any remaining grants continue to apply.
3233+
3234+
The available scopes are the same as [`process.permission.has()`][]:
3235+
3236+
* `fs` - All File System (drops both read and write)
3237+
* `fs.read` - File System read operations
3238+
* `fs.write` - File System write operations
3239+
* `child` - Child process spawning operations
3240+
* `worker` - Worker thread spawning operation
3241+
* `net` - Network operations
3242+
* `inspector` - Inspector operations
3243+
* `wasi` - WASI operations
3244+
* `addon` - Native addon operations
3245+
3246+
```js
3247+
const fs = require('node:fs');
3248+
3249+
// Read configuration during startup
3250+
const config = fs.readFileSync('/etc/myapp/config.json', 'utf8');
3251+
3252+
// Drop read access to the config directory after initialization
3253+
process.permission.drop('fs.read', '/etc/myapp');
3254+
3255+
// This will now throw ERR_ACCESS_DENIED
3256+
fs.readFileSync('/etc/myapp/config.json');
3257+
```
3258+
32003259
## `process.pid`
32013260
32023261
<!-- YAML
@@ -4601,6 +4660,7 @@ cases:
46014660
[`ChildProcess.disconnect()`]: child_process.md#subprocessdisconnect
46024661
[`ChildProcess.send()`]: child_process.md#subprocesssendmessage-sendhandle-options-callback
46034662
[`ChildProcess`]: child_process.md#class-childprocess
4663+
[`ERR_ACCESS_DENIED`]: errors.md#err_access_denied
46044664
[`Error`]: errors.md#class-error
46054665
[`EventEmitter`]: events.md#class-eventemitter
46064666
[`NODE_OPTIONS`]: cli.md#node_optionsoptions
@@ -4625,6 +4685,7 @@ cases:
46254685
[`process.hrtime()`]: #processhrtimetime
46264686
[`process.hrtime.bigint()`]: #processhrtimebigint
46274687
[`process.kill()`]: #processkillpid-signal
4688+
[`process.permission.has()`]: #processpermissionhasscope-reference
46284689
[`process.setUncaughtExceptionCaptureCallback()`]: #processsetuncaughtexceptioncapturecallbackfn
46294690
[`promise.catch()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
46304691
[`queueMicrotask()`]: globals.md#queuemicrotaskcallback

lib/internal/process/permission.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ module.exports = ObjectFreeze({
3333

3434
return permission.has(scope, reference);
3535
},
36+
drop(scope, reference) {
37+
validateString(scope, 'scope');
38+
if (reference != null) {
39+
if (isBuffer(reference)) {
40+
validateBuffer(reference, 'reference');
41+
} else {
42+
validateString(reference, 'reference');
43+
}
44+
}
45+
46+
permission.drop(scope, reference);
47+
},
3648
availableFlags() {
3749
return [
3850
'--allow-fs-read',

lib/internal/process/pre_execution.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ function initializePermission() {
651651
};
652652
// Guarantee path module isn't monkey-patched to bypass permission model
653653
ObjectFreeze(require('path'));
654-
const { has } = require('internal/process/permission');
654+
const { has, drop } = require('internal/process/permission');
655655
const warnFlags = [
656656
'--allow-addons',
657657
'--allow-child-process',
@@ -700,6 +700,7 @@ function initializePermission() {
700700
configurable: false,
701701
value: {
702702
has,
703+
drop,
703704
},
704705
});
705706
} else {

src/permission/addon_permission.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ void AddonPermission::Apply(Environment* env,
1414
deny_all_ = true;
1515
}
1616

17+
void AddonPermission::Drop(Environment* env,
18+
PermissionScope scope,
19+
const std::string_view& param) {
20+
deny_all_ = true;
21+
}
22+
1723
bool AddonPermission::is_granted(Environment* env,
1824
PermissionScope perm,
1925
const std::string_view& param) const {

src/permission/addon_permission.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ class AddonPermission final : public PermissionBase {
1515
void Apply(Environment* env,
1616
const std::vector<std::string>& allow,
1717
PermissionScope scope) override;
18+
void Drop(Environment* env,
19+
PermissionScope scope,
20+
const std::string_view& param = "") override;
1821
bool is_granted(Environment* env,
1922
PermissionScope perm,
2023
const std::string_view& param = "") const override;

src/permission/child_process_permission.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ void ChildProcessPermission::Apply(Environment* env,
1515
deny_all_ = true;
1616
}
1717

18+
void ChildProcessPermission::Drop(Environment* env,
19+
PermissionScope scope,
20+
const std::string_view& param) {
21+
deny_all_ = true;
22+
}
23+
1824
bool ChildProcessPermission::is_granted(Environment* env,
1925
PermissionScope perm,
2026
const std::string_view& param) const {

src/permission/child_process_permission.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ class ChildProcessPermission final : public PermissionBase {
1515
void Apply(Environment* env,
1616
const std::vector<std::string>& allow,
1717
PermissionScope scope) override;
18+
void Drop(Environment* env,
19+
PermissionScope scope,
20+
const std::string_view& param = "") override;
1821
bool is_granted(Environment* env,
1922
PermissionScope perm,
2023
const std::string_view& param = "") const override;

src/permission/fs_permission.cc

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,97 @@ void FSPermission::Apply(Environment* env,
154154
}
155155
}
156156

157+
void FSPermission::Drop(Environment* env,
158+
PermissionScope scope,
159+
const std::string_view& param) {
160+
if (param.empty()) {
161+
// Drop all access for this scope
162+
if (scope == PermissionScope::kFileSystemRead ||
163+
scope == PermissionScope::kFileSystem) {
164+
deny_all_in_ = true;
165+
allow_all_in_ = false;
166+
granted_in_fs_.Clear();
167+
granted_paths_in_.clear();
168+
}
169+
if (scope == PermissionScope::kFileSystemWrite ||
170+
scope == PermissionScope::kFileSystem) {
171+
deny_all_out_ = true;
172+
allow_all_out_ = false;
173+
granted_out_fs_.Clear();
174+
granted_paths_out_.clear();
175+
}
176+
return;
177+
}
178+
179+
// When allowed with *, you can only drop * (no specific paths)
180+
std::string resolved = PathResolve(env, {param});
181+
if (scope == PermissionScope::kFileSystemRead ||
182+
scope == PermissionScope::kFileSystem) {
183+
if (!allow_all_in_) {
184+
RevokeAccess(PermissionScope::kFileSystemRead, resolved);
185+
}
186+
}
187+
if (scope == PermissionScope::kFileSystemWrite ||
188+
scope == PermissionScope::kFileSystem) {
189+
if (!allow_all_out_) {
190+
RevokeAccess(PermissionScope::kFileSystemWrite, resolved);
191+
}
192+
}
193+
}
194+
195+
void FSPermission::RevokeAccess(PermissionScope perm,
196+
const std::string& res) {
197+
const std::string path = WildcardIfDir(res);
198+
if (perm == PermissionScope::kFileSystemRead) {
199+
auto it = std::find(granted_paths_in_.begin(),
200+
granted_paths_in_.end(), path);
201+
if (it != granted_paths_in_.end()) {
202+
granted_paths_in_.erase(it);
203+
RebuildTree(PermissionScope::kFileSystemRead);
204+
}
205+
} else if (perm == PermissionScope::kFileSystemWrite) {
206+
auto it = std::find(granted_paths_out_.begin(),
207+
granted_paths_out_.end(), path);
208+
if (it != granted_paths_out_.end()) {
209+
granted_paths_out_.erase(it);
210+
RebuildTree(PermissionScope::kFileSystemWrite);
211+
}
212+
}
213+
}
214+
215+
void FSPermission::RebuildTree(PermissionScope scope) {
216+
if (scope == PermissionScope::kFileSystemRead) {
217+
granted_in_fs_.Clear();
218+
if (granted_paths_in_.empty()) {
219+
deny_all_in_ = true;
220+
} else {
221+
for (const auto& path : granted_paths_in_) {
222+
granted_in_fs_.Insert(path);
223+
}
224+
}
225+
} else if (scope == PermissionScope::kFileSystemWrite) {
226+
granted_out_fs_.Clear();
227+
if (granted_paths_out_.empty()) {
228+
deny_all_out_ = true;
229+
} else {
230+
for (const auto& path : granted_paths_out_) {
231+
granted_out_fs_.Insert(path);
232+
}
233+
}
234+
}
235+
}
236+
157237
void FSPermission::GrantAccess(PermissionScope perm, const std::string& res) {
158238
const std::string path = WildcardIfDir(res);
159239
if (perm == PermissionScope::kFileSystemRead &&
160240
!granted_in_fs_.Lookup(path)) {
161241
granted_in_fs_.Insert(path);
242+
granted_paths_in_.push_back(path);
162243
deny_all_in_ = false;
163244
} else if (perm == PermissionScope::kFileSystemWrite &&
164245
!granted_out_fs_.Lookup(path)) {
165246
granted_out_fs_.Insert(path);
247+
granted_paths_out_.push_back(path);
166248
deny_all_out_ = false;
167249
}
168250
}
@@ -196,6 +278,16 @@ FSPermission::RadixTree::~RadixTree() {
196278
FreeRecursivelyNode(root_node_);
197279
}
198280

281+
void FSPermission::RadixTree::Clear() {
282+
for (auto& c : root_node_->children) {
283+
FreeRecursivelyNode(c.second);
284+
}
285+
root_node_->children.clear();
286+
delete root_node_->wildcard_child;
287+
root_node_->wildcard_child = nullptr;
288+
root_node_->is_leaf = false;
289+
}
290+
199291
bool FSPermission::RadixTree::Lookup(const std::string_view& s,
200292
bool when_empty_return) const {
201293
FSPermission::RadixTree::Node* current_node = root_node_;

src/permission/fs_permission.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ class FSPermission final : public PermissionBase {
1818
void Apply(Environment* env,
1919
const std::vector<std::string>& allow,
2020
PermissionScope scope) override;
21+
void Drop(Environment* env,
22+
PermissionScope scope,
23+
const std::string_view& param = "") override;
2124
bool is_granted(Environment* env,
2225
PermissionScope perm,
2326
const std::string_view& param) const override;
@@ -139,6 +142,7 @@ class FSPermission final : public PermissionBase {
139142
RadixTree();
140143
~RadixTree();
141144
void Insert(const std::string& s);
145+
void Clear();
142146
bool Lookup(const std::string_view& s) const { return Lookup(s, false); }
143147
bool Lookup(const std::string_view& s, bool when_empty_return) const;
144148

@@ -148,10 +152,15 @@ class FSPermission final : public PermissionBase {
148152

149153
private:
150154
void GrantAccess(PermissionScope scope, const std::string& param);
155+
void RevokeAccess(PermissionScope scope, const std::string& param);
156+
void RebuildTree(PermissionScope scope);
151157
// fs granted on startup
152158
RadixTree granted_in_fs_;
153159
RadixTree granted_out_fs_;
154160

161+
std::vector<std::string> granted_paths_in_;
162+
std::vector<std::string> granted_paths_out_;
163+
155164
bool deny_all_in_ = true;
156165
bool deny_all_out_ = true;
157166

0 commit comments

Comments
 (0)