Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

module: allowed module location list #34379

Closed
wants to merge 1 commit into from
Closed
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
14 changes: 14 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ If this flag is passed, the behavior can still be set to not abort through
[`process.setUncaughtExceptionCaptureCallback()`][] (and through usage of the
`domain` module that uses it).

### `--allowed-module-location`
<!-- YAML
added: REPLACE_ME
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
added: REPLACE_ME
added: REPLACEME

-->

Only allow loading modules from given path. Can be used multiple times to set
multiple allowed module locations.

If set, before loading any module Node.js will first check if given module is
inside any of the allowed directories (including sub-directories). If not it
will throw [`ERR_MODULE_BLOCKED`][] error.

### `--completion-bash`
<!-- YAML
added: v10.12.0
Expand Down Expand Up @@ -1222,6 +1234,7 @@ node --require "./a.js" --require "./b.js"

Node.js options that are allowed are:
<!-- node-options-node start -->
* `--allowed-module-location`
* `--disable-proto`
* `--enable-fips`
* `--enable-source-maps`
Expand Down Expand Up @@ -1567,6 +1580,7 @@ $ node --max-old-space-size=1536 index.js
[`unhandledRejection`]: process.html#process_event_unhandledrejection
[`worker_threads.threadId`]: worker_threads.html##worker_threads_worker_threadid
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
[`ERR_MODULE_BLOCKED`]: errors.html#err_module_blocked
[REPL]: repl.html
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage
[Source Map]: https://sourcemaps.info/spec.html
Expand Down
7 changes: 7 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1641,6 +1641,12 @@ The V8 platform used by this instance of Node.js does not support creating
Workers. This is caused by lack of embedder support for Workers. In particular,
this error will not occur with standard builds of Node.js.

<a id="ERR_MODULE_BLOCKED"></a>
### `ERR_MODULE_BLOCKED`

Module was found but loading was blocked because module path was not included in
the [`--allowed-module-location`][] option.

<a id="ERR_MODULE_NOT_FOUND"></a>
### `ERR_MODULE_NOT_FOUND`

Expand Down Expand Up @@ -2545,6 +2551,7 @@ Used when an attempt is made to use a `zlib` object after it has already been
closed.

[`'uncaughtException'`]: process.html#process_event_uncaughtexception
[`--allowed-module-location`]: cli.html#allowed_module_location
[`--disable-proto=throw`]: cli.html#cli_disable_proto_mode
[`--force-fips`]: cli.html#cli_force_fips
[`Class: assert.AssertionError`]: assert.html#assert_class_assert_assertionerror
Expand Down
6 changes: 6 additions & 0 deletions doc/api/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,10 @@ either be a core module or is loaded from a `node_modules` folder.
If the given path does not exist, `require()` will throw an [`Error`][] with its
`code` property set to `'MODULE_NOT_FOUND'`.

If [`--allowed-module-location`][] was used and the given file is not inside
one of the allowed module locations, `require()` will throw an
[`ERR_MODULE_BLOCKED`][] error.

## Folders as modules

<!--type=misc-->
Expand Down Expand Up @@ -1137,6 +1141,7 @@ consists of the following keys:
* originalLine: {number}
* originalColumn: {number}

[`--allowed-module-location`]: cli.html#allowed_module_location
[GLOBAL_FOLDERS]: #modules_loading_from_the_global_folders
[`Error`]: errors.html#errors_class_error
[`__dirname`]: #modules_dirname
Expand All @@ -1147,6 +1152,7 @@ consists of the following keys:
[`module.children`]: #modules_module_children
[`path.dirname()`]: path.html#path_path_dirname_path
[ECMAScript Modules]: esm.html
[`ERR_MODULE_BLOCKED`]: errors.html#err_module_blocked
[an error]: errors.html#errors_err_require_esm
[exports shortcut]: #modules_exports_shortcut
[module resolution]: #modules_all_together
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ Pass the rest of the arguments to the script.
If no script filename or eval/print script is supplied prior to this, then
the next argument will be used as a script filename.
.
.If Fl -allowed-module-list
Set allowed locations from which modules can be loaded (option can be repeated).
.
.It Fl -abort-on-uncaught-exception
Aborting instead of exiting causes a core file to be generated for analysis.
.
Expand Down
3 changes: 3 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,9 @@ E('ERR_MISSING_ARGS',
return `${msg} must be specified`;
}, TypeError);
E('ERR_MISSING_OPTION', '%s is required', TypeError);
E('ERR_MODULE_BLOCKED',
'Loading of module %s was blocked: module path is outside allowed locations',
Error);
E('ERR_MODULE_NOT_FOUND', (path, base, type = 'package') => {
return `Cannot find ${type} '${path}' imported from ${base}`;
}, Error);
Expand Down
24 changes: 24 additions & 0 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,15 @@ const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
const manifest = getOptionValue('--experimental-policy') ?
require('internal/process/policy').manifest :
null;
const allowedModuleLocations = getOptionValue('--allowed-module-location');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should cache builtins to avoid mutations from affecting this (comment 1 of n)

Suggested change
const allowedModuleLocations = getOptionValue('--allowed-module-location');
const allowedModuleLocations = getOptionValue('--allowed-module-location');
const { relative: PathRelative, isAbsolute: PathIsAbsolute } = path;

const { compileFunction } = internalBinding('contextify');

// Whether any user-provided CJS modules had been loaded (executed).
// Used for internal assertions.
let hasLoadedAnyUserCJSModule = false;

const {
ERR_MODULE_BLOCKED,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_OPT_VALUE,
ERR_INVALID_PACKAGE_CONFIG,
Expand Down Expand Up @@ -937,6 +939,28 @@ Module._load = function(request, parent, isMain) {
const mod = loadNativeModule(filename, request);
if (mod && mod.canBeRequiredByUsers) return mod.exports;

// Only return the file location if it is in an allowed location
// or the allowed locations are not set
let locationAllowed = true;
if (allowedModuleLocations && allowedModuleLocations.length > 0) {
locationAllowed = false;
debug('Allowed module locations count: %d', allowedModuleLocations.length);
for (const allowedLocation of allowedModuleLocations) {
const relative = path.relative(allowedLocation, filename);
debug('Relative path from allowed location "%s" to loaded "%s": %s',
allowedLocation, filename, relative);
if (relative === '' ||
(!relative.startsWith('..') && !path.isAbsolute(relative))) {
debug('Module will be allowed');
locationAllowed = true;
break;
}
}
}
Comment on lines +945 to +959
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should cache builtins to avoid mutations from affecting this (comment 2 of n)

Suggested change
if (allowedModuleLocations && allowedModuleLocations.length > 0) {
locationAllowed = false;
debug('Allowed module locations count: %d', allowedModuleLocations.length);
for (const allowedLocation of allowedModuleLocations) {
const relative = path.relative(allowedLocation, filename);
debug('Relative path from allowed location "%s" to loaded "%s": %s',
allowedLocation, filename, relative);
if (relative === '' ||
(!relative.startsWith('..') && !path.isAbsolute(relative))) {
debug('Module will be allowed');
locationAllowed = true;
break;
}
}
}
if (allowedModuleLocations && allowedModuleLocations.length > 0) {
locationAllowed = false;
debug('Allowed module locations count: %d', allowedModuleLocations.length);
// avoids mutation of Array.prototype[Symbol.iterator] from affecting this
for (let i = 0; i < allowedModuleLocations.length; i++) {
const allowedLocation = allowedModuleLocations[i];
const relative = PathRelative(allowedLocation, filename);
debug('Relative path from allowed location "%s" to loaded "%s": %s',
allowedLocation, filename, relative);
if (relative === '' ||
(!StringPrototypeStartsWith(relative, '..') && !PathIsAbsolute(relative))) {
debug('Module will be allowed');
locationAllowed = true;
break;
}
}
}

if (!locationAllowed) {
throw new ERR_MODULE_BLOCKED(request);
}

// Don't call updateChildren(), Module constructor already does.
const module = new Module(filename, parent);

Expand Down
5 changes: 5 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"set default TLS maximum to TLSv1.3 (default: TLSv1.3)",
&EnvironmentOptions::tls_max_v1_3,
kAllowedInEnvironment);

AddOption("--allowed-module-location",
"set allowed locations from which modules can be loaded (option can be repeated)",
&EnvironmentOptions::allowed_module_locations,
kAllowedInEnvironment);
}

PerIsolateOptionsParser::PerIsolateOptionsParser(
Expand Down
2 changes: 2 additions & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ class EnvironmentOptions : public Options {

std::vector<std::string> preload_modules;

std::vector<std::string> allowed_module_locations;

std::vector<std::string> user_argv;

inline DebugOptions* get_debug_options() { return &debug_options_; }
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/allowed-modules/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {}
3 changes: 3 additions & 0 deletions test/fixtures/allowed-modules/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"main": "index.js"
}
41 changes: 41 additions & 0 deletions test/parallel/test-allowed-module-location.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict';
// Flags: --expose-internals
require('../common');
const { getOptionValue } = require('internal/options');
const assert = require('assert');
const path = require('path');
const cp = require('child_process');

if (getOptionValue('--allowed-module-location').length === 0) {
const args = [
'--expose-internals',
'--allowed-module-location', __dirname,
'--allowed-module-location', path.join(__dirname, '..', 'common'),
__filename
];
const result = cp.spawnSync(process.argv0,
args,
{ stdio: 'inherit', shell: false });
assert.strictEqual(result.status, 0);
process.exit(0);
}

const requireModule = path.join('..', 'fixtures', 'allowed-modules');
assert.throws(
() => { require(requireModule); },
{
code: 'ERR_MODULE_BLOCKED',
name: 'Error',
message: `Loading of module ${requireModule} was blocked: ` +
'module path is outside allowed locations'
});

const requireFile = path.join(requireModule, 'index.js');
assert.throws(
() => { require(requireFile); },
{
code: 'ERR_MODULE_BLOCKED',
name: 'Error',
message: `Loading of module ${requireFile} was blocked: ` +
'module path is outside allowed locations'
});