src,permission: add multiple allow-fs-* flags

Support for a single comma separates list for allow-fs-* flags is
removed. Instead now multiple flags can be passed to allow multiple
paths.

Fixes: https://github.com/nodejs/security-wg/issues/1039
PR-URL: https://github.com/nodejs/node/pull/49047
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
This commit is contained in:
Carlos Espa 2023-08-17 20:39:04 +02:00 committed by GitHub
parent 3c6a1c6af4
commit 413c16e490
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 161 additions and 34 deletions

View File

@ -145,6 +145,10 @@ Error: Access to this API has been restricted
<!-- YAML <!-- YAML
added: v20.0.0 added: v20.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/49047
description: Paths delimited by comma (`,`) are no longer allowed.
--> -->
> Stability: 1 - Experimental > Stability: 1 - Experimental
@ -155,8 +159,11 @@ the [Permission Model][].
The valid arguments for the `--allow-fs-read` flag are: The valid arguments for the `--allow-fs-read` flag are:
* `*` - To allow all `FileSystemRead` operations. * `*` - To allow all `FileSystemRead` operations.
* Paths delimited by comma (`,`) to allow only matching `FileSystemRead` * Multiple paths can be allowed using multiple `--allow-fs-read` flags.
operations. Example `--allow-fs-read=/folder1/ --allow-fs-read=/folder1/`
Paths delimited by comma (`,`) are no longer allowed.
When passing a single flag with a comma a warning will be diplayed
Examples can be found in the [File System Permissions][] documentation. Examples can be found in the [File System Permissions][] documentation.
@ -192,6 +199,10 @@ node --experimental-permission --allow-fs-read=/path/to/index.js index.js
<!-- YAML <!-- YAML
added: v20.0.0 added: v20.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/49047
description: Paths delimited by comma (`,`) are no longer allowed.
--> -->
> Stability: 1 - Experimental > Stability: 1 - Experimental
@ -202,8 +213,11 @@ the [Permission Model][].
The valid arguments for the `--allow-fs-write` flag are: The valid arguments for the `--allow-fs-write` flag are:
* `*` - To allow all `FileSystemWrite` operations. * `*` - To allow all `FileSystemWrite` operations.
* Paths delimited by comma (`,`) to allow only matching `FileSystemWrite` * Multiple paths can be allowed using multiple `--allow-fs-read` flags.
operations. Example `--allow-fs-read=/folder1/ --allow-fs-read=/folder1/`
Paths delimited by comma (`,`) are no longer allowed.
When passing a single flag with a comma a warning will be diplayed
Examples can be found in the [File System Permissions][] documentation. Examples can be found in the [File System Permissions][] documentation.

View File

@ -532,7 +532,7 @@ Example:
* `--allow-fs-write=*` - It will allow all `FileSystemWrite` operations. * `--allow-fs-write=*` - It will allow all `FileSystemWrite` operations.
* `--allow-fs-write=/tmp/` - It will allow `FileSystemWrite` access to the `/tmp/` * `--allow-fs-write=/tmp/` - It will allow `FileSystemWrite` access to the `/tmp/`
folder. folder.
* `--allow-fs-read=/tmp/,/home/.gitignore` - It allows `FileSystemRead` access * `--allow-fs-read=/tmp/ --allow-fs-read=/home/.gitignore` - It allows `FileSystemRead` access
to the `/tmp/` folder **and** the `/home/.gitignore` path. to the `/tmp/` folder **and** the `/home/.gitignore` path.
Wildcards are supported too: Wildcards are supported too:

View File

@ -554,6 +554,22 @@ function initializePermission() {
'It could invalidate the permission model.', 'SecurityWarning'); 'It could invalidate the permission model.', 'SecurityWarning');
} }
} }
const warnCommaFlags = [
'--allow-fs-read',
'--allow-fs-write',
];
for (const flag of warnCommaFlags) {
const value = getOptionValue(flag);
if (value.length === 1 && value[0].includes(',')) {
process.emitWarning(
`The ${flag} CLI flag has changed. ` +
'Passing a comma-separated list of paths is no longer valid. ' +
'Documentation can be found at ' +
'https://nodejs.org/api/permissions.html#file-system-permissions',
'Warning',
);
}
}
ObjectDefineProperty(process, 'permission', { ObjectDefineProperty(process, 'permission', {
__proto__: null, __proto__: null,
@ -572,7 +588,8 @@ function initializePermission() {
'--allow-worker', '--allow-worker',
]; ];
ArrayPrototypeForEach(availablePermissionFlags, (flag) => { ArrayPrototypeForEach(availablePermissionFlags, (flag) => {
if (getOptionValue(flag)) { const value = getOptionValue(flag);
if (value.length) {
throw new ERR_MISSING_OPTION('--experimental-permission'); throw new ERR_MISSING_OPTION('--experimental-permission');
} }
}); });

View File

@ -875,12 +875,12 @@ Environment::Environment(IsolateData* isolate_data,
// unless explicitly allowed by the user // unless explicitly allowed by the user
options_->allow_native_addons = false; options_->allow_native_addons = false;
flags_ = flags_ | EnvironmentFlags::kNoCreateInspector; flags_ = flags_ | EnvironmentFlags::kNoCreateInspector;
permission()->Apply("*", permission::PermissionScope::kInspector); permission()->Apply({"*"}, permission::PermissionScope::kInspector);
if (!options_->allow_child_process) { if (!options_->allow_child_process) {
permission()->Apply("*", permission::PermissionScope::kChildProcess); permission()->Apply({"*"}, permission::PermissionScope::kChildProcess);
} }
if (!options_->allow_worker_threads) { if (!options_->allow_worker_threads) {
permission()->Apply("*", permission::PermissionScope::kWorkerThreads); permission()->Apply({"*"}, permission::PermissionScope::kWorkerThreads);
} }
if (!options_->allow_fs_read.empty()) { if (!options_->allow_fs_read.empty()) {

View File

@ -121,8 +121,8 @@ class EnvironmentOptions : public Options {
std::string experimental_policy_integrity; std::string experimental_policy_integrity;
bool has_policy_integrity_string = false; bool has_policy_integrity_string = false;
bool experimental_permission = false; bool experimental_permission = false;
std::string allow_fs_read; std::vector<std::string> allow_fs_read;
std::string allow_fs_write; std::vector<std::string> allow_fs_write;
bool allow_child_process = false; bool allow_child_process = false;
bool allow_worker_threads = false; bool allow_worker_threads = false;
bool experimental_repl_await = true; bool experimental_repl_await = true;

View File

@ -9,7 +9,7 @@ namespace permission {
// Currently, ChildProcess manage a single state // Currently, ChildProcess manage a single state
// Once denied, it's always denied // Once denied, it's always denied
void ChildProcessPermission::Apply(const std::string& allow, void ChildProcessPermission::Apply(const std::vector<std::string>& allow,
PermissionScope scope) { PermissionScope scope) {
deny_all_ = true; deny_all_ = true;
} }

View File

@ -12,7 +12,8 @@ namespace permission {
class ChildProcessPermission final : public PermissionBase { class ChildProcessPermission final : public PermissionBase {
public: public:
void Apply(const std::string& allow, PermissionScope scope) override; void Apply(const std::vector<std::string>& allow,
PermissionScope scope) override;
bool is_granted(PermissionScope perm, bool is_granted(PermissionScope perm,
const std::string_view& param = "") override; const std::string_view& param = "") override;

View File

@ -116,9 +116,11 @@ namespace permission {
// allow = '*' // allow = '*'
// allow = '/tmp/,/home/example.js' // allow = '/tmp/,/home/example.js'
void FSPermission::Apply(const std::string& allow, PermissionScope scope) { void FSPermission::Apply(const std::vector<std::string>& allow,
PermissionScope scope) {
using std::string_view_literals::operator""sv; using std::string_view_literals::operator""sv;
for (const std::string_view res : SplitString(allow, ","sv)) {
for (const std::string_view res : allow) {
if (res == "*"sv) { if (res == "*"sv) {
if (scope == PermissionScope::kFileSystemRead) { if (scope == PermissionScope::kFileSystemRead) {
deny_all_in_ = false; deny_all_in_ = false;

View File

@ -15,7 +15,8 @@ namespace permission {
class FSPermission final : public PermissionBase { class FSPermission final : public PermissionBase {
public: public:
void Apply(const std::string& allow, PermissionScope scope) override; void Apply(const std::vector<std::string>& allow,
PermissionScope scope) override;
bool is_granted(PermissionScope perm, const std::string_view& param) override; bool is_granted(PermissionScope perm, const std::string_view& param) override;
struct RadixTree { struct RadixTree {

View File

@ -8,7 +8,7 @@ namespace permission {
// Currently, Inspector manage a single state // Currently, Inspector manage a single state
// Once denied, it's always denied // Once denied, it's always denied
void InspectorPermission::Apply(const std::string& allow, void InspectorPermission::Apply(const std::vector<std::string>& allow,
PermissionScope scope) { PermissionScope scope) {
deny_all_ = true; deny_all_ = true;
} }

View File

@ -12,7 +12,8 @@ namespace permission {
class InspectorPermission final : public PermissionBase { class InspectorPermission final : public PermissionBase {
public: public:
void Apply(const std::string& allow, PermissionScope scope) override; void Apply(const std::vector<std::string>& allow,
PermissionScope scope) override;
bool is_granted(PermissionScope perm, bool is_granted(PermissionScope perm,
const std::string_view& param = "") override; const std::string_view& param = "") override;

View File

@ -130,7 +130,8 @@ void Permission::EnablePermissions() {
} }
} }
void Permission::Apply(const std::string& allow, PermissionScope scope) { void Permission::Apply(const std::vector<std::string>& allow,
PermissionScope scope) {
auto permission = nodes_.find(scope); auto permission = nodes_.find(scope);
if (permission != nodes_.end()) { if (permission != nodes_.end()) {
permission->second->Apply(allow, scope); permission->second->Apply(allow, scope);

View File

@ -49,7 +49,7 @@ class Permission {
const std::string_view& res); const std::string_view& res);
// CLI Call // CLI Call
void Apply(const std::string& allow, PermissionScope scope); void Apply(const std::vector<std::string>& allow, PermissionScope scope);
void EnablePermissions(); void EnablePermissions();
private: private:

View File

@ -39,7 +39,8 @@ enum class PermissionScope {
class PermissionBase { class PermissionBase {
public: public:
virtual void Apply(const std::string& allow, PermissionScope scope) = 0; virtual void Apply(const std::vector<std::string>& allow,
PermissionScope scope) = 0;
virtual bool is_granted(PermissionScope perm, virtual bool is_granted(PermissionScope perm,
const std::string_view& param = "") = 0; const std::string_view& param = "") = 0;
}; };

View File

@ -9,7 +9,8 @@ namespace permission {
// Currently, PolicyDenyWorker manage a single state // Currently, PolicyDenyWorker manage a single state
// Once denied, it's always denied // Once denied, it's always denied
void WorkerPermission::Apply(const std::string& allow, PermissionScope scope) { void WorkerPermission::Apply(const std::vector<std::string>& allow,
PermissionScope scope) {
deny_all_ = true; deny_all_ = true;
} }

View File

@ -12,7 +12,8 @@ namespace permission {
class WorkerPermission final : public PermissionBase { class WorkerPermission final : public PermissionBase {
public: public:
void Apply(const std::string& allow, PermissionScope scope) override; void Apply(const std::vector<std::string>& allow,
PermissionScope scope) override;
bool is_granted(PermissionScope perm, bool is_granted(PermissionScope perm,
const std::string_view& param = "") override; const std::string_view& param = "") override;

View File

@ -31,7 +31,9 @@ describe('legacyMainResolve', () => {
for (const [mainOrFolder, allowReads] of paths) { for (const [mainOrFolder, allowReads] of paths) {
const allowReadFilePaths = allowReads.map((filepath) => path.resolve(fixtextureFolder, filepath)); const allowReadFilePaths = allowReads.map((filepath) => path.resolve(fixtextureFolder, filepath));
const allowReadFiles = allowReads?.length > 0 ? ['--allow-fs-read', allowReadFilePaths.join(',')] : []; const allowReadFiles = allowReads?.length > 0 ?
allowReadFilePaths.flatMap((path) => ['--allow-fs-read', path]) :
[];
const fixtextureFolderEscaped = escapeWhenSepIsBackSlash(fixtextureFolder); const fixtextureFolderEscaped = escapeWhenSepIsBackSlash(fixtextureFolder);
const { status, stderr } = spawnSync( const { status, stderr } = spawnSync(
@ -85,7 +87,9 @@ describe('legacyMainResolve', () => {
for (const [folder, expectedFile, allowReads] of paths) { for (const [folder, expectedFile, allowReads] of paths) {
const allowReadFilePaths = allowReads.map((filepath) => path.resolve(fixtextureFolder, folder, filepath)); const allowReadFilePaths = allowReads.map((filepath) => path.resolve(fixtextureFolder, folder, filepath));
const allowReadFiles = allowReads?.length > 0 ? ['--allow-fs-read', allowReadFilePaths.join(',')] : []; const allowReadFiles = allowReads?.length > 0 ?
allowReadFilePaths.flatMap((path) => ['--allow-fs-read', path]) :
[];
const fixtextureFolderEscaped = escapeWhenSepIsBackSlash(fixtextureFolder); const fixtextureFolderEscaped = escapeWhenSepIsBackSlash(fixtextureFolder);
const { status, stderr } = spawnSync( const { status, stderr } = spawnSync(

View File

@ -0,0 +1,83 @@
'use strict';
require('../common');
const { spawnSync } = require('child_process');
const assert = require('assert');
const path = require('path');
{
const tmpPath = path.resolve('/tmp/');
const otherPath = path.resolve('/other-path/');
const { status, stdout } = spawnSync(
process.execPath,
[
'--experimental-permission',
'--allow-fs-write', tmpPath, '--allow-fs-write', otherPath, '-e',
`console.log(process.permission.has("fs"));
console.log(process.permission.has("fs.read"));
console.log(process.permission.has("fs.write"));
console.log(process.permission.has("fs.write", "/tmp/"));
console.log(process.permission.has("fs.write", "/other-path/"));`,
]
);
const [fs, fsIn, fsOut, fsOutAllowed1, fsOutAllowed2] = stdout.toString().split('\n');
assert.strictEqual(fs, 'false');
assert.strictEqual(fsIn, 'false');
assert.strictEqual(fsOut, 'false');
assert.strictEqual(fsOutAllowed1, 'true');
assert.strictEqual(fsOutAllowed2, 'true');
assert.strictEqual(status, 0);
}
{
const tmpPath = path.resolve('/tmp/');
const pathWithComma = path.resolve('/other,path/');
const { status, stdout } = spawnSync(
process.execPath,
[
'--experimental-permission',
'--allow-fs-write',
tmpPath,
'--allow-fs-write',
pathWithComma,
'-e',
`console.log(process.permission.has("fs"));
console.log(process.permission.has("fs.read"));
console.log(process.permission.has("fs.write"));
console.log(process.permission.has("fs.write", "/tmp/"));
console.log(process.permission.has("fs.write", "/other,path/"));`,
]
);
const [fs, fsIn, fsOut, fsOutAllowed1, fsOutAllowed2] = stdout.toString().split('\n');
assert.strictEqual(fs, 'false');
assert.strictEqual(fsIn, 'false');
assert.strictEqual(fsOut, 'false');
assert.strictEqual(fsOutAllowed1, 'true');
assert.strictEqual(fsOutAllowed2, 'true');
assert.strictEqual(status, 0);
}
{
const filePath = path.resolve('/tmp/file,with,comma.txt');
const { status, stdout, stderr } = spawnSync(
process.execPath,
[
'--experimental-permission',
'--allow-fs-read=*',
`--allow-fs-write=${filePath}`,
'-e',
`console.log(process.permission.has("fs"));
console.log(process.permission.has("fs.read"));
console.log(process.permission.has("fs.write"));
console.log(process.permission.has("fs.write", "/tmp/file,with,comma.txt"));`,
]
);
const [fs, fsIn, fsOut, fsOutAllowed] = stdout.toString().split('\n');
assert.strictEqual(fs, 'false');
assert.strictEqual(fsIn, 'true');
assert.strictEqual(fsOut, 'false');
assert.strictEqual(fsOutAllowed, 'true');
assert.strictEqual(status, 0);
assert.ok(stderr.toString().includes('Warning: The --allow-fs-write CLI flag has changed.'));
}

View File

@ -28,7 +28,7 @@ const commonPath = path.join(__filename, '../../common');
const { status, stderr } = spawnSync( const { status, stderr } = spawnSync(
process.execPath, process.execPath,
[ [
'--experimental-permission', `--allow-fs-read=${file},${commonPathWildcard}`, file, '--experimental-permission', `--allow-fs-read=${file}`, `--allow-fs-read=${commonPathWildcard}`, file,
], ],
{ {
env: { env: {

View File

@ -36,8 +36,8 @@ fs.writeFileSync(path.join(readWriteFolder, 'file'), 'NO evil file contents');
process.execPath, process.execPath,
[ [
'--experimental-permission', '--experimental-permission',
`--allow-fs-read=${file},${commonPathWildcard},${readOnlyFolder},${readWriteFolder}`, `--allow-fs-read=${file}`, `--allow-fs-read=${commonPathWildcard}`, `--allow-fs-read=${readOnlyFolder}`, `--allow-fs-read=${readWriteFolder}`,
`--allow-fs-write=${readWriteFolder},${writeOnlyFolder}`, `--allow-fs-write=${readWriteFolder}`, `--allow-fs-write=${writeOnlyFolder}`,
file, file,
], ],
{ {

View File

@ -37,7 +37,7 @@ const symlinkFromBlockedFile = tmpdir.resolve('example-symlink.md');
process.execPath, process.execPath,
[ [
'--experimental-permission', '--experimental-permission',
`--allow-fs-read=${file},${commonPathWildcard},${symlinkFromBlockedFile}`, `--allow-fs-read=${file}`, `--allow-fs-read=${commonPathWildcard}`, `--allow-fs-read=${symlinkFromBlockedFile}`,
`--allow-fs-write=${symlinkFromBlockedFile}`, `--allow-fs-write=${symlinkFromBlockedFile}`,
file, file,
], ],

View File

@ -31,7 +31,7 @@ const commonPathWildcard = path.join(__filename, '../../common*');
process.execPath, process.execPath,
[ [
'--experimental-permission', '--experimental-permission',
`--allow-fs-read=${file},${commonPathWildcard},${allowedFolder}`, `--allow-fs-read=${file}`, `--allow-fs-read=${commonPathWildcard}`, `--allow-fs-read=${allowedFolder}`,
`--allow-fs-write=${allowedFolder}`, `--allow-fs-write=${allowedFolder}`,
file, file,
], ],

View File

@ -32,7 +32,7 @@ if (common.isWindows) {
process.execPath, process.execPath,
[ [
'--experimental-permission', '--experimental-permission',
`--allow-fs-read=${allowList.join(',')}`, ...allowList.flatMap((path) => ['--allow-fs-read', path]),
'-e', '-e',
` `
const path = require('path'); const path = require('path');
@ -67,7 +67,7 @@ if (common.isWindows) {
process.execPath, process.execPath,
[ [
'--experimental-permission', '--experimental-permission',
`--allow-fs-read=${allowList.join(',')}`, ...allowList.flatMap((path) => ['--allow-fs-read', path]),
'-e', '-e',
` `
const assert = require('assert') const assert = require('assert')
@ -92,7 +92,7 @@ if (common.isWindows) {
process.execPath, process.execPath,
[ [
'--experimental-permission', '--experimental-permission',
`--allow-fs-read=${file},${commonPathWildcard},${allowList.join(',')}`, `--allow-fs-read=${file}`, `--allow-fs-read=${commonPathWildcard}`, ...allowList.flatMap((path) => ['--allow-fs-read', path]),
file, file,
], ],
); );

View File

@ -26,7 +26,7 @@ const file = fixtures.path('permission', 'fs-write.js');
[ [
'--experimental-permission', '--experimental-permission',
'--allow-fs-read=*', '--allow-fs-read=*',
`--allow-fs-write=${regularFile},${commonPath}`, `--allow-fs-write=${regularFile}`, `--allow-fs-write=${commonPath}`,
file, file,
], ],
{ {