Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion doc/api/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ flag. For WASI, use the [`--allow-wasi`][] flag.

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

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

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

##### `permission.drop(scope[, reference])`

API call to drop permissions at runtime. This operation is **irreversible**.

When called without a reference, the entire scope is dropped. When called
with a reference, only the permission for that specific resource is revoked.

You can only drop the exact resource that was explicitly granted. The
reference passed to `drop()` must match the original grant. If a permission
was granted using a wildcard (`*`), only the entire scope can be dropped
(by calling `drop()` without a reference). If a directory was granted
(e.g. `--allow-fs-read=/my/folder`), you cannot drop individual files
inside it - you must drop the same directory that was originally granted.

```js
const fs = require('node:fs');

// Read config at startup while we still have permission
const config = fs.readFileSync('/etc/myapp/config.json', 'utf8');

// Drop read access to /etc/myapp after initialization
process.permission.drop('fs.read', '/etc/myapp');

// This will now throw ERR_ACCESS_DENIED
process.permission.has('fs.read', '/etc/myapp/config.json'); // false

// Drop child process permission entirely
process.permission.drop('child');
```

#### File System Permissions

The Permission Model, by default, restricts access to the file system through the `node:fs` module.
Expand Down
60 changes: 60 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -3197,6 +3197,65 @@ process.permission.has('fs.read', './README.md');
process.permission.has('fs.read');
```

### `process.permission.drop(scope[, reference])`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.1 - Active Development

* `scope` {string}
* `reference` {string}

Drops the specified permission from the current process. This operation is
**irreversible** — once a permission is dropped, it cannot be restored through
any Node.js API.

If no reference is provided, the entire scope is dropped. For example,
`process.permission.drop('fs.read')` will revoke ALL file system read
permissions.

When a reference is provided, only the permission for that specific resource
is dropped. For example, `process.permission.drop('fs.read', '/etc/myapp')`
will revoke read access to that directory while keeping other read
permissions intact.

**Important:** You can only drop the exact resource that was explicitly
granted. The reference passed to `drop()` must match the original grant:

* If a permission was granted using a wildcard (`*`), such as
`--allow-fs-read=*`, individual paths cannot be dropped - only the entire
scope can be dropped (by calling `drop()` without a reference).
* If a directory was granted (e.g. `--allow-fs-read=/my/folder`), you cannot
drop access to individual files inside it. You must drop the same directory
that was granted. Any remaining grants continue to apply.

The available scopes are the same as [`process.permission.has()`][]:

* `fs` - All File System (drops both read and write)
* `fs.read` - File System read operations
* `fs.write` - File System write operations
* `child` - Child process spawning operations
* `worker` - Worker thread spawning operation
* `net` - Network operations
* `inspector` - Inspector operations
* `wasi` - WASI operations
* `addon` - Native addon operations

```js
const fs = require('node:fs');

// Read configuration during startup
const config = fs.readFileSync('/etc/myapp/config.json', 'utf8');

// Drop read access to the config directory after initialization
process.permission.drop('fs.read', '/etc/myapp');

// This will now throw ERR_ACCESS_DENIED
fs.readFileSync('/etc/myapp/config.json');
```

## `process.pid`

<!-- YAML
Expand Down Expand Up @@ -4625,6 +4684,7 @@ cases:
[`process.hrtime()`]: #processhrtimetime
[`process.hrtime.bigint()`]: #processhrtimebigint
[`process.kill()`]: #processkillpid-signal
[`process.permission.has()`]: #processpermissionhasscope-reference
[`process.setUncaughtExceptionCaptureCallback()`]: #processsetuncaughtexceptioncapturecallbackfn
[`promise.catch()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
[`queueMicrotask()`]: globals.md#queuemicrotaskcallback
Expand Down
12 changes: 12 additions & 0 deletions lib/internal/process/permission.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ module.exports = ObjectFreeze({

return permission.has(scope, reference);
},
drop(scope, reference) {
validateString(scope, 'scope');
if (reference != null) {
if (isBuffer(reference)) {
validateBuffer(reference, 'reference');
} else {
validateString(reference, 'reference');
}
}

permission.drop(scope, reference);
},
availableFlags() {
return [
'--allow-fs-read',
Expand Down
3 changes: 2 additions & 1 deletion lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ function initializePermission() {
};
// Guarantee path module isn't monkey-patched to bypass permission model
ObjectFreeze(require('path'));
const { has } = require('internal/process/permission');
const { has, drop } = require('internal/process/permission');
const warnFlags = [
'--allow-addons',
'--allow-child-process',
Expand Down Expand Up @@ -700,6 +700,7 @@ function initializePermission() {
configurable: false,
value: {
has,
drop,
},
});
} else {
Expand Down
6 changes: 6 additions & 0 deletions src/permission/addon_permission.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ void AddonPermission::Apply(Environment* env,
deny_all_ = true;
}

void AddonPermission::Drop(Environment* env,
PermissionScope scope,
const std::string_view& param) {
deny_all_ = true;
}

bool AddonPermission::is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param) const {
Expand Down
3 changes: 3 additions & 0 deletions src/permission/addon_permission.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class AddonPermission final : public PermissionBase {
void Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) override;
void Drop(Environment* env,
PermissionScope scope,
const std::string_view& param = "") override;
bool is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param = "") const override;
Expand Down
6 changes: 6 additions & 0 deletions src/permission/child_process_permission.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ void ChildProcessPermission::Apply(Environment* env,
deny_all_ = true;
}

void ChildProcessPermission::Drop(Environment* env,
PermissionScope scope,
const std::string_view& param) {
deny_all_ = true;
}

bool ChildProcessPermission::is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param) const {
Expand Down
3 changes: 3 additions & 0 deletions src/permission/child_process_permission.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class ChildProcessPermission final : public PermissionBase {
void Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) override;
void Drop(Environment* env,
PermissionScope scope,
const std::string_view& param = "") override;
bool is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param = "") const override;
Expand Down
91 changes: 91 additions & 0 deletions src/permission/fs_permission.cc
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,96 @@ void FSPermission::Apply(Environment* env,
}
}

void FSPermission::Drop(Environment* env,
PermissionScope scope,
const std::string_view& param) {
if (param.empty()) {
// Drop all access for this scope
if (scope == PermissionScope::kFileSystemRead ||
scope == PermissionScope::kFileSystem) {
deny_all_in_ = true;
allow_all_in_ = false;
granted_in_fs_.Clear();
granted_paths_in_.clear();
}
if (scope == PermissionScope::kFileSystemWrite ||
scope == PermissionScope::kFileSystem) {
deny_all_out_ = true;
allow_all_out_ = false;
granted_out_fs_.Clear();
granted_paths_out_.clear();
}
return;
}

// When allowed with *, you can only drop * (no specific paths)
std::string resolved = PathResolve(env, {param});
if (scope == PermissionScope::kFileSystemRead ||
scope == PermissionScope::kFileSystem) {
if (!allow_all_in_) {
RevokeAccess(PermissionScope::kFileSystemRead, resolved);
}
}
if (scope == PermissionScope::kFileSystemWrite ||
scope == PermissionScope::kFileSystem) {
if (!allow_all_out_) {
RevokeAccess(PermissionScope::kFileSystemWrite, resolved);
}
}
}

void FSPermission::RevokeAccess(PermissionScope perm, const std::string& res) {
const std::string path = WildcardIfDir(res);
if (perm == PermissionScope::kFileSystemRead) {
auto it =
std::find(granted_paths_in_.begin(), granted_paths_in_.end(), path);
if (it != granted_paths_in_.end()) {
granted_paths_in_.erase(it);
RebuildTree(PermissionScope::kFileSystemRead);
}
} else if (perm == PermissionScope::kFileSystemWrite) {
auto it =
std::find(granted_paths_out_.begin(), granted_paths_out_.end(), path);
if (it != granted_paths_out_.end()) {
granted_paths_out_.erase(it);
RebuildTree(PermissionScope::kFileSystemWrite);
}
}
}

void FSPermission::RebuildTree(PermissionScope scope) {
if (scope == PermissionScope::kFileSystemRead) {
granted_in_fs_.Clear();
if (granted_paths_in_.empty()) {
deny_all_in_ = true;
} else {
for (const auto& path : granted_paths_in_) {
granted_in_fs_.Insert(path);
}
}
} else if (scope == PermissionScope::kFileSystemWrite) {
granted_out_fs_.Clear();
if (granted_paths_out_.empty()) {
deny_all_out_ = true;
} else {
for (const auto& path : granted_paths_out_) {
granted_out_fs_.Insert(path);
}
}
}
}

void FSPermission::GrantAccess(PermissionScope perm, const std::string& res) {
const std::string path = WildcardIfDir(res);
if (perm == PermissionScope::kFileSystemRead &&
!granted_in_fs_.Lookup(path)) {
granted_in_fs_.Insert(path);
granted_paths_in_.push_back(path);
deny_all_in_ = false;
} else if (perm == PermissionScope::kFileSystemWrite &&
!granted_out_fs_.Lookup(path)) {
granted_out_fs_.Insert(path);
granted_paths_out_.push_back(path);
deny_all_out_ = false;
}
}
Expand Down Expand Up @@ -196,6 +277,16 @@ FSPermission::RadixTree::~RadixTree() {
FreeRecursivelyNode(root_node_);
}

void FSPermission::RadixTree::Clear() {
for (auto& c : root_node_->children) {
FreeRecursivelyNode(c.second);
}
root_node_->children.clear();
delete root_node_->wildcard_child;
root_node_->wildcard_child = nullptr;
root_node_->is_leaf = false;
}

bool FSPermission::RadixTree::Lookup(const std::string_view& s,
bool when_empty_return) const {
FSPermission::RadixTree::Node* current_node = root_node_;
Expand Down
9 changes: 9 additions & 0 deletions src/permission/fs_permission.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class FSPermission final : public PermissionBase {
void Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) override;
void Drop(Environment* env,
PermissionScope scope,
const std::string_view& param = "") override;
bool is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param) const override;
Expand Down Expand Up @@ -139,6 +142,7 @@ class FSPermission final : public PermissionBase {
RadixTree();
~RadixTree();
void Insert(const std::string& s);
void Clear();
bool Lookup(const std::string_view& s) const { return Lookup(s, false); }
bool Lookup(const std::string_view& s, bool when_empty_return) const;

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

private:
void GrantAccess(PermissionScope scope, const std::string& param);
void RevokeAccess(PermissionScope scope, const std::string& param);
void RebuildTree(PermissionScope scope);
// fs granted on startup
RadixTree granted_in_fs_;
RadixTree granted_out_fs_;

std::vector<std::string> granted_paths_in_;
std::vector<std::string> granted_paths_out_;

bool deny_all_in_ = true;
bool deny_all_out_ = true;

Expand Down
6 changes: 6 additions & 0 deletions src/permission/inspector_permission.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ void InspectorPermission::Apply(Environment* env,
deny_all_ = true;
}

void InspectorPermission::Drop(Environment* env,
PermissionScope scope,
const std::string_view& param) {
deny_all_ = true;
}

bool InspectorPermission::is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param) const {
Expand Down
3 changes: 3 additions & 0 deletions src/permission/inspector_permission.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class InspectorPermission final : public PermissionBase {
void Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) override;
void Drop(Environment* env,
PermissionScope scope,
const std::string_view& param = "") override;
bool is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param = "") const override;
Expand Down
6 changes: 6 additions & 0 deletions src/permission/net_permission.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ void NetPermission::Apply(Environment* env,
allow_net_ = true;
}

void NetPermission::Drop(Environment* env,
PermissionScope scope,
const std::string_view& param) {
allow_net_ = false;
}

bool NetPermission::is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param) const {
Expand Down
3 changes: 3 additions & 0 deletions src/permission/net_permission.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class NetPermission final : public PermissionBase {
void Apply(Environment* env,
const std::vector<std::string>& allow,
PermissionScope scope) override;
void Drop(Environment* env,
PermissionScope scope,
const std::string_view& param = "") override;
bool is_granted(Environment* env,
PermissionScope perm,
const std::string_view& param) const override;
Expand Down
Loading
Loading