| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -74,7 +74,7 @@ flag. For WASI, use the [`--allow-wasi`][] flag. | |||
| 74 | 74 | ||
| 75 | 75 | When enabling the Permission Model through the [`--permission`][] | |
| 76 | 76 | flag a new property `permission` is added to the `process` object. | |
| 77 | - This property contains one function: | ||
| 77 | + This property contains the following functions: | ||
| 78 | 78 | ||
| 79 | 79 | ##### `permission.has(scope[, reference])` | |
| 80 | 80 | ||
@@ -88,6 +88,40 @@ process.permission.has('fs.read'); // true | |||
| 88 | 88 | process.permission.has('fs.read', '/home/rafaelgss/protected-folder'); // false | |
| 89 | 89 | ``` | |
| 90 | 90 | ||
| 91 | + ##### `permission.drop(scope[, reference])` | ||
| 92 | + | ||
| 93 | + API call to drop permissions at runtime. This operation is **irreversible**. | ||
| 94 | + | ||
| 95 | + When called without a reference, the entire scope is dropped. When called | ||
| 96 | + with a reference, only the permission for that specific resource is revoked. | ||
| 97 | + Dropping a permission only affects future access checks. It does not close or | ||
| 98 | + revoke access to resources that are already open, such as file descriptors, | ||
| 99 | + child processes, or worker threads. Applications are responsible for closing | ||
| 100 | + or terminating those resources when they are no longer needed. | ||
| 101 | + | ||
| 102 | + You can only drop the exact resource that was explicitly granted. The | ||
| 103 | + reference passed to `drop()` must match the original grant. If a permission | ||
| 104 | + was granted using a wildcard (`*`), only the entire scope can be dropped | ||
| 105 | + (by calling `drop()` without a reference). If a directory was granted | ||
| 106 | + (e.g. `--allow-fs-read=/my/folder`), you cannot drop individual files | ||
| 107 | + inside it - you must drop the same directory that was originally granted. | ||
| 108 | + | ||
| 109 | + ```js | ||
| 110 | + const fs = require('node:fs'); | ||
| 111 | + | ||
| 112 | + // Read config at startup while we still have permission | ||
| 113 | + const config = fs.readFileSync('/etc/myapp/config.json', 'utf8'); | ||
| 114 | + | ||
| 115 | + // Drop read access to /etc/myapp after initialization | ||
| 116 | + process.permission.drop('fs.read', '/etc/myapp'); | ||
| 117 | + | ||
| 118 | + // This will now throw ERR_ACCESS_DENIED | ||
| 119 | + process.permission.has('fs.read', '/etc/myapp/config.json'); // false | ||
| 120 | + | ||
| 121 | + // Drop child process permission entirely | ||
| 122 | + process.permission.drop('child'); | ||
| 123 | + ``` | ||
| 124 | + | ||
| 91 | 125 | #### File System Permissions | |
| 92 | 126 | ||
| 93 | 127 | The Permission Model, by default, restricts access to the file system through the `node:fs` module. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3189,6 +3189,64 @@ process.permission.has('fs.read', './README.md'); | |||
| 3189 | 3189 | process.permission.has('fs.read'); | |
| 3190 | 3190 | ``` | |
| 3191 | 3191 | ||
| 3192 | + ### `process.permission.drop(scope[, reference])` | ||
| 3193 | + | ||
| 3194 | + <!-- YAML | ||
| 3195 | + added: REPLACEME | ||
| 3196 | + --> | ||
| 3197 | + | ||
| 3198 | + > Stability: 1.1 - Active Development | ||
| 3199 | + | ||
| 3200 | + * `scope` {string} | ||
| 3201 | + * `reference` {string} | ||
| 3202 | + | ||
| 3203 | + Drops the specified permission from the current process. This operation is | ||
| 3204 | + **irreversible** — once a permission is dropped, it cannot be restored through | ||
| 3205 | + any Node.js API. | ||
| 3206 | + | ||
| 3207 | + If no reference is provided, the entire scope is dropped. For example, | ||
| 3208 | + `process.permission.drop('fs.read')` will revoke ALL file system read | ||
| 3209 | + permissions. | ||
| 3210 | + | ||
| 3211 | + When a reference is provided, only the permission for that specific resource | ||
| 3212 | + is dropped. For example, `process.permission.drop('fs.read', '/etc/myapp')` | ||
| 3213 | + will revoke read access to that directory while keeping other read | ||
| 3214 | + permissions intact. | ||
| 3215 | + | ||
| 3216 | + **Important:** You can only drop the exact resource that was explicitly | ||
| 3217 | + granted. The reference passed to `drop()` must match the original grant: | ||
| 3218 | + | ||
| 3219 | + * If a permission was granted using a wildcard (`*`), such as | ||
| 3220 | + `--allow-fs-read=*`, individual paths cannot be dropped - only the entire | ||
| 3221 | + scope can be dropped (by calling `drop()` without a reference). | ||
| 3222 | + * If a directory was granted (e.g. `--allow-fs-read=/my/folder`), you cannot | ||
| 3223 | + drop access to individual files inside it. You must drop the same directory | ||
| 3224 | + that was granted. Any remaining grants continue to apply. | ||
| 3225 | + | ||
| 3226 | + The available scopes are the same as [`process.permission.has()`][]: | ||
| 3227 | + | ||
| 3228 | + * `fs` - All File System (drops both read and write) | ||
| 3229 | + * `fs.read` - File System read operations | ||
| 3230 | + * `fs.write` - File System write operations | ||
| 3231 | + * `child` - Child process spawning operations | ||
| 3232 | + * `worker` - Worker thread spawning operation | ||
| 3233 | + * `inspector` - Inspector operations | ||
| 3234 | + * `wasi` - WASI operations | ||
| 3235 | + * `addon` - Native addon operations | ||
| 3236 | + | ||
| 3237 | + ```js | ||
| 3238 | + const fs = require('node:fs'); | ||
| 3239 | + | ||
| 3240 | + // Read configuration during startup | ||
| 3241 | + const config = fs.readFileSync('/etc/myapp/config.json', 'utf8'); | ||
| 3242 | + | ||
| 3243 | + // Drop read access to the config directory after initialization | ||
| 3244 | + process.permission.drop('fs.read', '/etc/myapp'); | ||
| 3245 | + | ||
| 3246 | + // This will now throw ERR_ACCESS_DENIED | ||
| 3247 | + fs.readFileSync('/etc/myapp/config.json'); | ||
| 3248 | + ``` | ||
| 3249 | + | ||
| 3192 | 3250 | ## `process.pid` | |
| 3193 | 3251 | ||
| 3194 | 3252 | <!-- YAML | |
@@ -4598,6 +4656,7 @@ cases: | |||
| 4598 | 4656 | [`process.hrtime()`]: #processhrtimetime | |
| 4599 | 4657 | [`process.hrtime.bigint()`]: #processhrtimebigint | |
| 4600 | 4658 | [`process.kill()`]: #processkillpid-signal | |
| 4659 | + [`process.permission.has()`]: #processpermissionhasscope-reference | ||
| 4601 | 4660 | [`process.setUncaughtExceptionCaptureCallback()`]: #processsetuncaughtexceptioncapturecallbackfn | |
| 4602 | 4661 | [`promise.catch()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch | |
| 4603 | 4662 | [`queueMicrotask()`]: globals.md#queuemicrotaskcallback | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -33,6 +33,18 @@ module.exports = ObjectFreeze({ | |||
| 33 | 33 | ||
| 34 | 34 | return permission.has(scope, reference); | |
| 35 | 35 | }, | |
| 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 | + }, | ||
| 36 | 48 | availableFlags() { | |
| 37 | 49 | return [ | |
| 38 | 50 | '--allow-fs-read', | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -641,7 +641,7 @@ function initializePermission() { | |||
| 641 | 641 | }; | |
| 642 | 642 | // Guarantee path module isn't monkey-patched to bypass permission model | |
| 643 | 643 | ObjectFreeze(require('path')); | |
| 644 | - const { has } = require('internal/process/permission'); | ||
| 644 | + const { has, drop } = require('internal/process/permission'); | ||
| 645 | 645 | const warnFlags = [ | |
| 646 | 646 | '--allow-addons', | |
| 647 | 647 | '--allow-child-process', | |
@@ -679,6 +679,7 @@ function initializePermission() { | |||
| 679 | 679 | configurable: false, | |
| 680 | 680 | value: { | |
| 681 | 681 | has, | |
| 682 | + drop, | ||
| 682 | 683 | }, | |
| 683 | 684 | }); | |
| 684 | 685 | } else { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,6 +14,12 @@ void AddonPermission::Apply(Environment* env, | |||
| 14 | 14 | deny_all_ = true; | |
| 15 | 15 | } | |
| 16 | 16 | ||
| 17 | + void AddonPermission::Drop(Environment* env, | ||
| 18 | + PermissionScope scope, | ||
| 19 | + const std::string_view& param) { | ||
| 20 | + deny_all_ = true; | ||
| 21 | + } | ||
| 22 | + | ||
| 17 | 23 | bool AddonPermission::is_granted(Environment* env, | |
| 18 | 24 | PermissionScope perm, | |
| 19 | 25 | const std::string_view& param) const { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,6 +15,9 @@ class AddonPermission final : public PermissionBase { | |||
| 15 | 15 | void Apply(Environment* env, | |
| 16 | 16 | const std::vector<std::string>& allow, | |
| 17 | 17 | PermissionScope scope) override; | |
| 18 | + void Drop(Environment* env, | ||
| 19 | + PermissionScope scope, | ||
| 20 | + const std::string_view& param = "") override; | ||
| 18 | 21 | bool is_granted(Environment* env, | |
| 19 | 22 | PermissionScope perm, | |
| 20 | 23 | const std::string_view& param = "") const override; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,6 +15,12 @@ void ChildProcessPermission::Apply(Environment* env, | |||
| 15 | 15 | deny_all_ = true; | |
| 16 | 16 | } | |
| 17 | 17 | ||
| 18 | + void ChildProcessPermission::Drop(Environment* env, | ||
| 19 | + PermissionScope scope, | ||
| 20 | + const std::string_view& param) { | ||
| 21 | + deny_all_ = true; | ||
| 22 | + } | ||
| 23 | + | ||
| 18 | 24 | bool ChildProcessPermission::is_granted(Environment* env, | |
| 19 | 25 | PermissionScope perm, | |
| 20 | 26 | const std::string_view& param) const { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,6 +15,9 @@ class ChildProcessPermission final : public PermissionBase { | |||
| 15 | 15 | void Apply(Environment* env, | |
| 16 | 16 | const std::vector<std::string>& allow, | |
| 17 | 17 | PermissionScope scope) override; | |
| 18 | + void Drop(Environment* env, | ||
| 19 | + PermissionScope scope, | ||
| 20 | + const std::string_view& param = "") override; | ||
| 18 | 21 | bool is_granted(Environment* env, | |
| 19 | 22 | PermissionScope perm, | |
| 20 | 23 | const std::string_view& param = "") const override; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -154,15 +154,96 @@ void FSPermission::Apply(Environment* env, | |||
| 154 | 154 | } | |
| 155 | 155 | } | |
| 156 | 156 | ||
| 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, const std::string& res) { | ||
| 196 | + const std::string path = WildcardIfDir(res); | ||
| 197 | + if (perm == PermissionScope::kFileSystemRead) { | ||
| 198 | + auto it = | ||
| 199 | + std::find(granted_paths_in_.begin(), granted_paths_in_.end(), path); | ||
| 200 | + if (it != granted_paths_in_.end()) { | ||
| 201 | + granted_paths_in_.erase(it); | ||
| 202 | + RebuildTree(PermissionScope::kFileSystemRead); | ||
| 203 | + } | ||
| 204 | + } else if (perm == PermissionScope::kFileSystemWrite) { | ||
| 205 | + auto it = | ||
| 206 | + std::find(granted_paths_out_.begin(), granted_paths_out_.end(), path); | ||
| 207 | + if (it != granted_paths_out_.end()) { | ||
| 208 | + granted_paths_out_.erase(it); | ||
| 209 | + RebuildTree(PermissionScope::kFileSystemWrite); | ||
| 210 | + } | ||
| 211 | + } | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + void FSPermission::RebuildTree(PermissionScope scope) { | ||
| 215 | + if (scope == PermissionScope::kFileSystemRead) { | ||
| 216 | + granted_in_fs_.Clear(); | ||
| 217 | + if (granted_paths_in_.empty()) { | ||
| 218 | + deny_all_in_ = true; | ||
| 219 | + } else { | ||
| 220 | + for (const auto& path : granted_paths_in_) { | ||
| 221 | + granted_in_fs_.Insert(path); | ||
| 222 | + } | ||
| 223 | + } | ||
| 224 | + } else if (scope == PermissionScope::kFileSystemWrite) { | ||
| 225 | + granted_out_fs_.Clear(); | ||
| 226 | + if (granted_paths_out_.empty()) { | ||
| 227 | + deny_all_out_ = true; | ||
| 228 | + } else { | ||
| 229 | + for (const auto& path : granted_paths_out_) { | ||
| 230 | + granted_out_fs_.Insert(path); | ||
| 231 | + } | ||
| 232 | + } | ||
| 233 | + } | ||
| 234 | + } | ||
| 235 | + | ||
| 157 | 236 | void FSPermission::GrantAccess(PermissionScope perm, const std::string& res) { | |
| 158 | 237 | const std::string path = WildcardIfDir(res); | |
| 159 | 238 | if (perm == PermissionScope::kFileSystemRead && | |
| 160 | 239 | !granted_in_fs_.Lookup(path)) { | |
| 161 | 240 | granted_in_fs_.Insert(path); | |
| 241 | + granted_paths_in_.push_back(path); | ||
| 162 | 242 | deny_all_in_ = false; | |
| 163 | 243 | } else if (perm == PermissionScope::kFileSystemWrite && | |
| 164 | 244 | !granted_out_fs_.Lookup(path)) { | |
| 165 | 245 | granted_out_fs_.Insert(path); | |
| 246 | + granted_paths_out_.push_back(path); | ||
| 166 | 247 | deny_all_out_ = false; | |
| 167 | 248 | } | |
| 168 | 249 | } | |
@@ -196,6 +277,16 @@ FSPermission::RadixTree::~RadixTree() { | |||
| 196 | 277 | FreeRecursivelyNode(root_node_); | |
| 197 | 278 | } | |
| 198 | 279 | ||
| 280 | + void FSPermission::RadixTree::Clear() { | ||
| 281 | + for (auto& c : root_node_->children) { | ||
| 282 | + FreeRecursivelyNode(c.second); | ||
| 283 | + } | ||
| 284 | + root_node_->children.clear(); | ||
| 285 | + delete root_node_->wildcard_child; | ||
| 286 | + root_node_->wildcard_child = nullptr; | ||
| 287 | + root_node_->is_leaf = false; | ||
| 288 | + } | ||
| 289 | + | ||
| 199 | 290 | bool FSPermission::RadixTree::Lookup(const std::string_view& s, | |
| 200 | 291 | bool when_empty_return) const { | |
| 201 | 292 | FSPermission::RadixTree::Node* current_node = root_node_; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -18,6 +18,9 @@ class FSPermission final : public PermissionBase { | |||
| 18 | 18 | void Apply(Environment* env, | |
| 19 | 19 | const std::vector<std::string>& allow, | |
| 20 | 20 | PermissionScope scope) override; | |
| 21 | + void Drop(Environment* env, | ||
| 22 | + PermissionScope scope, | ||
| 23 | + const std::string_view& param = "") override; | ||
| 21 | 24 | bool is_granted(Environment* env, | |
| 22 | 25 | PermissionScope perm, | |
| 23 | 26 | const std::string_view& param) const override; | |
@@ -142,6 +145,7 @@ class FSPermission final : public PermissionBase { | |||
| 142 | 145 | RadixTree(); | |
| 143 | 146 | ~RadixTree(); | |
| 144 | 147 | void Insert(const std::string& s); | |
| 148 | + void Clear(); | ||
| 145 | 149 | bool Lookup(const std::string_view& s) const { return Lookup(s, false); } | |
| 146 | 150 | bool Lookup(const std::string_view& s, bool when_empty_return) const; | |
| 147 | 151 | ||
@@ -151,10 +155,15 @@ class FSPermission final : public PermissionBase { | |||
| 151 | 155 | ||
| 152 | 156 | private: | |
| 153 | 157 | void GrantAccess(PermissionScope scope, const std::string& param); | |
| 158 | + void RevokeAccess(PermissionScope scope, const std::string& param); | ||
| 159 | + void RebuildTree(PermissionScope scope); | ||
| 154 | 160 | // fs granted on startup | |
| 155 | 161 | RadixTree granted_in_fs_; | |
| 156 | 162 | RadixTree granted_out_fs_; | |
| 157 | 163 | ||
| 164 | + std::vector<std::string> granted_paths_in_; | ||
| 165 | + std::vector<std::string> granted_paths_out_; | ||
| 166 | + | ||
| 158 | 167 | bool deny_all_in_ = true; | |
| 159 | 168 | bool deny_all_out_ = true; | |
| 160 | 169 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments