diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index 1fc59fdeffd510..739f7181b61df1 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -341,6 +341,7 @@ require('url'); require('internal/options'); if (config.hasOpenSSL) { require('crypto'); + require('internal/process/policy'); } function setupPrepareStackTrace() { diff --git a/lib/internal/bootstrap/pre_execution.js b/lib/internal/bootstrap/pre_execution.js index 98428f02b43817..1e869837412ca5 100644 --- a/lib/internal/bootstrap/pre_execution.js +++ b/lib/internal/bootstrap/pre_execution.js @@ -25,6 +25,8 @@ const { const { Buffer } = require('buffer'); const { ERR_MANIFEST_ASSERT_INTEGRITY } = require('internal/errors').codes; const assert = require('internal/assert'); +const policy = require('internal/process/policy'); +const SRI = require('internal/policy/sri'); function prepareMainThreadExecution(expandArgv1 = false) { // TODO(joyeecheung): this is also necessary for workers when they deserialize @@ -434,7 +436,9 @@ function initializeClusterIPC() { } function initializePolicy() { - const experimentalPolicy = getOptionValue('--experimental-policy'); + const experimentalPolicy = getOptionValue('[has_experimental_policy_string]') ? + getOptionValue('--experimental-policy') : + null; if (experimentalPolicy) { process.emitWarning('Policies are experimental.', 'ExperimentalWarning'); @@ -453,7 +457,6 @@ function initializePolicy() { const src = fs.readFileSync(manifestURL, 'utf8'); const experimentalPolicyIntegrity = getOptionValue('--policy-integrity'); if (experimentalPolicyIntegrity) { - const SRI = require('internal/policy/sri'); const { createHash, timingSafeEqual } = require('crypto'); const realIntegrities = new SafeMap(); const integrityEntries = SRI.parse(experimentalPolicyIntegrity); @@ -477,9 +480,17 @@ function initializePolicy() { throw new ERR_MANIFEST_ASSERT_INTEGRITY(manifestURL, realIntegrities); } } - require('internal/process/policy') - .setup(src, manifestURL.href); + policy.setup(src, manifestURL.href); + } else { + // This disables the policy TDZ checks so code can just check + // if (!policy.manifest) + // it is important to keep the policy in TDZ until the user supplied + // options are determined at startup + policy.setup(null); } + // Ensure that policy no longer has a TDZ by checking + // that we can access .manifest policy.manifest + policy.manifest; // eslint-disable-line no-unused-expressions } function initializeWASI() { diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js index a8167b86ca2e5a..190a3699677d2d 100644 --- a/lib/internal/main/worker_thread.js +++ b/lib/internal/main/worker_thread.js @@ -63,6 +63,7 @@ let debug = require('internal/util/debuglog').debuglog('worker', (fn) => { }); const assert = require('internal/assert'); +const policy = require('internal/process/policy'); patchProcessObject(); setupInspectorHooks(); @@ -124,7 +125,9 @@ port.on('message', (message) => { setupPerfHooks(); initializeReport(); if (manifestSrc) { - require('internal/process/policy').setup(manifestSrc, manifestURL); + policy.setup(manifestSrc, manifestURL); + } else { + policy.setup(null); } initializeDeprecations(); initializeWASI(); diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 9e4229af9faaf8..b71a42743f016c 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -101,9 +101,7 @@ const { getOptionValue } = require('internal/options'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); // Do not eagerly grab .manifest, it may be in TDZ -const policy = getOptionValue('--experimental-policy') ? - require('internal/process/policy') : - null; +const policy = require('internal/process/policy'); // Whether any user-provided CJS modules had been loaded (executed). // Used for internal assertions. @@ -1053,7 +1051,7 @@ function wrapSafe(filename, content, cjsModuleInstance) { Module.prototype._compile = function(content, filename) { let moduleURL; let redirects; - if (policy?.manifest) { + if (policy.manifest) { moduleURL = pathToFileURL(filename); redirects = policy.manifest.getDependencyMapper(moduleURL); policy.manifest.assertIntegrity(moduleURL, content); @@ -1158,7 +1156,7 @@ Module._extensions['.js'] = function(module, filename) { Module._extensions['.json'] = function(module, filename) { const content = fs.readFileSync(filename, 'utf8'); - if (policy?.manifest) { + if (policy.manifest) { const moduleURL = pathToFileURL(filename); policy.manifest.assertIntegrity(moduleURL, content); } @@ -1174,7 +1172,7 @@ Module._extensions['.json'] = function(module, filename) { // Native extension for .node Module._extensions['.node'] = function(module, filename) { - if (policy?.manifest) { + if (policy.manifest) { const content = fs.readFileSync(filename); const moduleURL = pathToFileURL(filename); policy.manifest.assertIntegrity(moduleURL, content); diff --git a/lib/internal/modules/esm/get_source.js b/lib/internal/modules/esm/get_source.js index ab2a9888f76fe7..d1526c30f402bf 100644 --- a/lib/internal/modules/esm/get_source.js +++ b/lib/internal/modules/esm/get_source.js @@ -9,9 +9,7 @@ const { getOptionValue } = require('internal/options'); const { fetchModule } = require('internal/modules/esm/fetch_module'); // Do not eagerly grab .manifest, it may be in TDZ -const policy = getOptionValue('--experimental-policy') ? - require('internal/process/policy') : - null; +const policy = require('internal/process/policy'); const experimentalNetworkImports = getOptionValue('--experimental-network-imports'); @@ -52,7 +50,7 @@ async function defaultGetSource(url, context, defaultGetSource) { experimentalNetworkImports ? ['https', 'http'] : [], ])); } - if (policy?.manifest) { + if (policy.manifest) { policy.manifest.assertIntegrity(parsed, source); } return source; diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 59d3bc1723e074..a6cfa3ba338b4f 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -10,6 +10,7 @@ const { ObjectFreeze, ObjectGetOwnPropertyNames, ObjectPrototypeHasOwnProperty, + ReflectApply, RegExp, RegExpPrototypeExec, RegExpPrototypeSymbolReplace, @@ -35,9 +36,7 @@ const { } = require('fs'); const { getOptionValue } = require('internal/options'); // Do not eagerly grab .manifest, it may be in TDZ -const policy = getOptionValue('--experimental-policy') ? - require('internal/process/policy') : - null; +const policy = require('internal/process/policy'); const { sep, relative, resolve } = require('path'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); @@ -1090,7 +1089,7 @@ function throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports) { async function defaultResolve(specifier, context = {}, defaultResolveUnused) { let { parentURL, conditions } = context; - if (parentURL && policy?.manifest) { + if (parentURL && policy.manifest) { const redirects = policy.manifest.getDependencyMapper(parentURL); if (redirects) { const { resolve, reaction } = redirects; @@ -1217,9 +1216,33 @@ async function defaultResolve(specifier, context = {}, defaultResolveUnused) { }; } +function policyWrappingDefaultResolve() { + const $defaultResolve = defaultResolve; + if (policy.manifest) { + module.exports.defaultResolve = async function defaultResolve( + specifier, + context + ) { + const ret = await $defaultResolve(specifier, context, $defaultResolve); + // This is a preflight check to avoid data exfiltration by query params etc. + policy.manifest.mightAllow(ret.url, () => + new ERR_MANIFEST_DEPENDENCY_MISSING( + context.parentURL, + specifier, + context.conditions + ) + ); + return ret; + }; + } else { + module.exports.defaultResolve = $defaultResolve; + } + return ReflectApply(module.exports.defaultResolve, this, arguments); +} + module.exports = { DEFAULT_CONDITIONS, - defaultResolve, + defaultResolve: policyWrappingDefaultResolve, encodedSepRegEx, getPackageScopeConfig, getPackageType, @@ -1231,22 +1254,3 @@ module.exports = { const { defaultGetFormatWithoutErrors, } = require('internal/modules/esm/get_format'); - -if (policy) { - const $defaultResolve = defaultResolve; - module.exports.defaultResolve = async function defaultResolve( - specifier, - context - ) { - const ret = await $defaultResolve(specifier, context, $defaultResolve); - // This is a preflight check to avoid data exfiltration by query params etc. - policy.manifest.mightAllow(ret.url, () => - new ERR_MANIFEST_DEPENDENCY_MISSING( - context.parentURL, - specifier, - context.conditions - ) - ); - return ret; - }; -} diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js index 09eb12bd1533bf..230306d61f534a 100644 --- a/lib/internal/modules/package_json_reader.js +++ b/lib/internal/modules/package_json_reader.js @@ -4,6 +4,7 @@ const { SafeMap } = primordials; const { internalModuleReadJSON } = internalBinding('fs'); const { pathToFileURL } = require('url'); const { toNamespacedPath } = require('path'); +const policy = require('internal/process/policy'); const cache = new SafeMap(); @@ -22,12 +23,9 @@ function read(jsonPath) { toNamespacedPath(jsonPath) ); const result = { string, containsKeys }; - const { getOptionValue } = require('internal/options'); if (string !== undefined) { if (manifest === undefined) { - manifest = getOptionValue('--experimental-policy') ? - require('internal/process/policy').manifest : - null; + manifest = policy.manifest; } if (manifest !== null) { const jsonURL = pathToFileURL(jsonPath); diff --git a/lib/internal/policy/manifest.js b/lib/internal/policy/manifest.js index a0cd9707a2c7d5..08b71f6306837f 100644 --- a/lib/internal/policy/manifest.js +++ b/lib/internal/policy/manifest.js @@ -38,9 +38,7 @@ const HashDigest = uncurryThis(crypto.Hash.prototype.digest); const BufferToString = uncurryThis(Buffer.prototype.toString); const kRelativeURLStringPattern = /^\.{0,2}\//; const { getOptionValue } = require('internal/options'); -const shouldAbortOnUncaughtException = getOptionValue( - '--abort-on-uncaught-exception' -); +let shouldAbortOnUncaughtException; const { abort, exit, _rawDebug } = process; // #endregion @@ -354,6 +352,14 @@ function findScopeHREF(href, scopeStore, allowSame) { * @typedef {boolean | string | SRI[] | typeof kCascade} Integrity */ +// To get this in the snapshot we can't have things that use getOptionsValue or +// expose C++ functions so we do these lazily +function lazyBits() { + shouldAbortOnUncaughtException = getOptionValue( + '--abort-on-uncaught-exception' + ); +} + class Manifest { #defaultDependencies; /** @@ -423,6 +429,7 @@ class Manifest { * @param {string} manifestHREF */ constructor(obj, manifestHREF) { + lazyBits(); this.href = manifestHREF; const scopes = this.#scopeDependencies; const integrities = this.#resourceIntegrities; diff --git a/lib/internal/policy/sri.js b/lib/internal/policy/sri.js index 22704704a310bc..c8ed4066625af8 100644 --- a/lib/internal/policy/sri.js +++ b/lib/internal/policy/sri.js @@ -33,7 +33,11 @@ ObjectSeal(kAllWSP); const BufferFrom = require('buffer').Buffer.from; -// Returns {algorithm, value (in base64 string), options,}[] +/** + * Strictly parses an SRI string and returns all found entries + * @param {string} str + * @returns {Array<{algorithm:string, value:string, options: string | null}>} + */ const parse = (str) => { let prevIndex = 0; let match; diff --git a/lib/internal/process/policy.js b/lib/internal/process/policy.js index ea283a449742fc..5b122fb6fa1d0c 100644 --- a/lib/internal/process/policy.js +++ b/lib/internal/process/policy.js @@ -10,17 +10,33 @@ const { ERR_MANIFEST_TDZ, } = require('internal/errors').codes; const { Manifest } = require('internal/policy/manifest'); +/** + * @type {InstanceType} + */ let manifest; +/** + * @type {string} + */ let manifestSrc; +/** + * @type {string} + */ let manifestURL; module.exports = ObjectFreeze({ __proto__: null, + /** + * @param {string | null} src null to not use a policy + * @param {string} url + * @returns {void} + */ setup(src, url) { manifestSrc = src; manifestURL = url; if (src === null) { manifest = null; + manifestSrc = null; + manifestURL = null; return; } diff --git a/lib/internal/worker.js b/lib/internal/worker.js index 1d2cd8cefd2996..206533928f6df9 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -38,7 +38,7 @@ const { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, } = errorCodes; -const { getOptionValue } = require('internal/options'); +const policy = require('internal/process/policy'); const workerIo = require('internal/worker/io'); const { @@ -241,12 +241,8 @@ class Worker extends EventEmitter { workerData: options.workerData, environmentData, publicPort: port2, - manifestURL: getOptionValue('--experimental-policy') ? - require('internal/process/policy').url : - null, - manifestSrc: getOptionValue('--experimental-policy') ? - require('internal/process/policy').src : - null, + manifestURL: policy.url, + manifestSrc: policy.src, hasStdin: !!options.stdin }, transferList); // Use this to cache the Worker's loopStart value once available. diff --git a/src/node_options.cc b/src/node_options.cc index 35ee54d223176a..072626e765a3e2 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -344,11 +344,15 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "experimental ES Module import.meta.resolve() support", &EnvironmentOptions::experimental_import_meta_resolve, kAllowedInEnvironment); + AddOption("[has_experimental_policy_string]", + "", + &EnvironmentOptions::has_experimental_policy_string); AddOption("--experimental-policy", "use the specified file as a " "security policy", &EnvironmentOptions::experimental_policy, kAllowedInEnvironment); + Implies("--experimental-policy", "[has_experimental_policy_string]"); AddOption("[has_policy_integrity_string]", "", &EnvironmentOptions::has_policy_integrity_string); diff --git a/src/node_options.h b/src/node_options.h index 3335c12b8cdcf7..fee627b4870eeb 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -114,6 +114,7 @@ class EnvironmentOptions : public Options { bool experimental_wasm_modules = false; bool experimental_import_meta_resolve = false; std::string module_type; + bool has_experimental_policy_string = false; std::string experimental_policy; std::string experimental_policy_integrity; bool has_policy_integrity_string = false; diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 04883cdf4cfbe0..bf72ad743d6216 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -99,6 +99,9 @@ const expectedModules = new Set([ 'NativeModule internal/perf/timerify', 'NativeModule internal/perf/usertiming', 'NativeModule internal/perf/utils', + 'NativeModule internal/policy/manifest', + 'NativeModule internal/policy/sri', + 'NativeModule internal/process/policy', 'NativeModule internal/priority_queue', 'NativeModule internal/process/esm_loader', 'NativeModule internal/process/execution',