lib,src: remove --experimental-policy

Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
PR-URL: https://github.com/nodejs/node/pull/52583
Refs: https://github.com/nodejs/node/issues/52575
Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
This commit is contained in:
Rafael Gonzaga 2024-05-07 13:25:45 -03:00 committed by GitHub
parent 9a1df15ee7
commit 951af83033
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
90 changed files with 116 additions and 5720 deletions

View File

@ -1,51 +0,0 @@
// Tests the impact on eager operations required for policies affecting
// general startup, does not test lazy operations
'use strict';
const common = require('../common.js');
const configs = {
n: [1024],
};
const options = {
flags: ['--expose-internals'],
};
const bench = common.createBenchmark(main, configs, options);
function main(conf) {
const hash = (str, algo) => {
const hash = require('crypto').createHash(algo);
return hash.update(str).digest('base64');
};
const resources = Object.fromEntries(
// Simulate graph of 1k modules
Array.from({ length: 1024 }, (_, i) => {
return [`./_${i}`, {
integrity: `sha256-${hash(`// ./_${i}`, 'sha256')}`,
dependencies: Object.fromEntries(Array.from({
// Average 3 deps per 4 modules
length: Math.floor((i % 4) / 2),
}, (_, ii) => {
return [`_${ii}`, `./_${i - ii}`];
})),
}];
}),
);
const json = JSON.parse(JSON.stringify({ resources }), (_, o) => {
if (o && typeof o === 'object') {
Reflect.setPrototypeOf(o, null);
Object.freeze(o);
}
return o;
});
const { Manifest } = require('internal/policy/manifest');
bench.start();
for (let i = 0; i < conf.n; i++) {
new Manifest(json, 'file://benchmark/policy-relative');
}
bench.end(conf.n);
}

View File

@ -881,16 +881,6 @@ following permissions are restricted:
* Child Process - manageable through [`--allow-child-process`][] flag
* Worker Threads - manageable through [`--allow-worker`][] flag
### `--experimental-policy`
<!-- YAML
added: v11.8.0
-->
> Stability: 0 - Deprecated: Will be removed shortly.
Use the specified file as a security policy.
### `--experimental-require-module`
<!-- YAML
@ -1508,18 +1498,6 @@ unless either the `--pending-deprecation` command-line flag, or the
are used to provide a kind of selective "early warning" mechanism that
developers may leverage to detect deprecated API usage.
### `--policy-integrity=sri`
<!-- YAML
added: v12.7.0
-->
> Stability: 0 - Deprecated: Will be removed shortly.
Instructs Node.js to error prior to running any code if the policy does not have
the specified integrity. It expects a [Subresource Integrity][] string as a
parameter.
### `--preserve-symlinks`
<!-- YAML
@ -2622,7 +2600,6 @@ one is included in the list below.
* `--experimental-modules`
* `--experimental-network-imports`
* `--experimental-permission`
* `--experimental-policy`
* `--experimental-print-required-tla`
* `--experimental-require-module`
* `--experimental-shadow-realm`
@ -2664,7 +2641,6 @@ one is included in the list below.
* `--openssl-legacy-provider`
* `--openssl-shared-config`
* `--pending-deprecation`
* `--policy-integrity`
* `--preserve-symlinks-main`
* `--preserve-symlinks`
* `--prof-process`
@ -3136,7 +3112,6 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage
[ShadowRealm]: https://github.com/tc39/proposal-shadowrealm
[Source Map]: https://sourcemaps.info/spec.html
[Subresource Integrity]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
[V8 JavaScript code coverage]: https://v8project.blogspot.com/2017/12/javascript-code-coverage.html
[V8 code cache]: https://v8.dev/blog/code-caching-for-devs
[`"type"`]: packages.md#type

View File

@ -2354,7 +2354,7 @@ Type: Documentation-only (supports [`--pending-deprecation`][])
`process.binding()` is for use by Node.js internal code only.
While `process.binding()` has not reached End-of-Life status in general, it is
unavailable when [policies][] or the [permission model][] are enabled.
unavailable when the [permission model][] is enabled.
### DEP0112: `dgram` private APIs
@ -3836,7 +3836,6 @@ is deprecated to better align with recommendations per [NIST SP 800-38D][].
[legacy URL API]: url.md#legacy-url-api
[legacy `urlObject`]: url.md#legacy-urlobject
[permission model]: permissions.md#permission-model
[policies]: permissions.md#policies
[static methods of `crypto.Certificate()`]: crypto.md#class-certificate
[subpath exports]: packages.md#subpath-exports
[subpath imports]: packages.md#subpath-imports

View File

@ -2159,68 +2159,6 @@ added:
An ESM loader hook returned without calling `next()` and without explicitly
signaling a short circuit.
<a id="ERR_MANIFEST_ASSERT_INTEGRITY"></a>
### `ERR_MANIFEST_ASSERT_INTEGRITY`
An attempt was made to load a resource, but the resource did not match the
integrity defined by the policy manifest. See the documentation for [policy][]
manifests for more information.
<a id="ERR_MANIFEST_DEPENDENCY_MISSING"></a>
### `ERR_MANIFEST_DEPENDENCY_MISSING`
An attempt was made to load a resource, but the resource was not listed as a
dependency from the location that attempted to load it. See the documentation
for [policy][] manifests for more information.
<a id="ERR_MANIFEST_INTEGRITY_MISMATCH"></a>
### `ERR_MANIFEST_INTEGRITY_MISMATCH`
An attempt was made to load a policy manifest, but the manifest had multiple
entries for a resource which did not match each other. Update the manifest
entries to match in order to resolve this error. See the documentation for
[policy][] manifests for more information.
<a id="ERR_MANIFEST_INVALID_RESOURCE_FIELD"></a>
### `ERR_MANIFEST_INVALID_RESOURCE_FIELD`
A policy manifest resource had an invalid value for one of its fields. Update
the manifest entry to match in order to resolve this error. See the
documentation for [policy][] manifests for more information.
<a id="ERR_MANIFEST_INVALID_SPECIFIER"></a>
### `ERR_MANIFEST_INVALID_SPECIFIER`
A policy manifest resource had an invalid value for one of its dependency
mappings. Update the manifest entry to match to resolve this error. See the
documentation for [policy][] manifests for more information.
<a id="ERR_MANIFEST_PARSE_POLICY"></a>
### `ERR_MANIFEST_PARSE_POLICY`
An attempt was made to load a policy manifest, but the manifest was unable to
be parsed. See the documentation for [policy][] manifests for more information.
<a id="ERR_MANIFEST_TDZ"></a>
### `ERR_MANIFEST_TDZ`
An attempt was made to read from a policy manifest, but the manifest
initialization has not yet taken place. This is likely a bug in Node.js.
<a id="ERR_MANIFEST_UNKNOWN_ONERROR"></a>
### `ERR_MANIFEST_UNKNOWN_ONERROR`
A policy manifest was loaded, but had an unknown value for its "onerror"
behavior. See the documentation for [policy][] manifests for more information.
<a id="ERR_MEMORY_ALLOCATION_FAILED"></a>
### `ERR_MEMORY_ALLOCATION_FAILED`
@ -3436,6 +3374,100 @@ removed: v21.1.0
An import attribute is not supported by this version of Node.js.
<a id="ERR_MANIFEST_ASSERT_INTEGRITY"></a>
### `ERR_MANIFEST_ASSERT_INTEGRITY`
<!-- YAML
removed: REPLACEME
-->
An attempt was made to load a resource, but the resource did not match the
integrity defined by the policy manifest. See the documentation for policy
manifests for more information.
<a id="ERR_MANIFEST_DEPENDENCY_MISSING"></a>
### `ERR_MANIFEST_DEPENDENCY_MISSING`
<!-- YAML
removed: REPLACEME
-->
An attempt was made to load a resource, but the resource was not listed as a
dependency from the location that attempted to load it. See the documentation
for policy manifests for more information.
<a id="ERR_MANIFEST_INTEGRITY_MISMATCH"></a>
### `ERR_MANIFEST_INTEGRITY_MISMATCH`
<!-- YAML
removed: REPLACEME
-->
An attempt was made to load a policy manifest, but the manifest had multiple
entries for a resource which did not match each other. Update the manifest
entries to match in order to resolve this error. See the documentation for
policy manifests for more information.
<a id="ERR_MANIFEST_INVALID_RESOURCE_FIELD"></a>
### `ERR_MANIFEST_INVALID_RESOURCE_FIELD`
<!-- YAML
removed: REPLACEME
-->
A policy manifest resource had an invalid value for one of its fields. Update
the manifest entry to match in order to resolve this error. See the
documentation for policy manifests for more information.
<a id="ERR_MANIFEST_INVALID_SPECIFIER"></a>
### `ERR_MANIFEST_INVALID_SPECIFIER`
<!-- YAML
removed: REPLACEME
-->
A policy manifest resource had an invalid value for one of its dependency
mappings. Update the manifest entry to match to resolve this error. See the
documentation for policy manifests for more information.
<a id="ERR_MANIFEST_PARSE_POLICY"></a>
### `ERR_MANIFEST_PARSE_POLICY`
<!-- YAML
removed: REPLACEME
-->
An attempt was made to load a policy manifest, but the manifest was unable to
be parsed. See the documentation for policy manifests for more information.
<a id="ERR_MANIFEST_TDZ"></a>
### `ERR_MANIFEST_TDZ`
<!-- YAML
removed: REPLACEME
-->
An attempt was made to read from a policy manifest, but the manifest
initialization has not yet taken place. This is likely a bug in Node.js.
<a id="ERR_MANIFEST_UNKNOWN_ONERROR"></a>
### `ERR_MANIFEST_UNKNOWN_ONERROR`
<!-- YAML
removed: REPLACEME
-->
A policy manifest was loaded, but had an unknown value for its "onerror"
behavior. See the documentation for policy manifests for more information.
<a id="ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST"></a>
### `ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`
@ -4016,7 +4048,6 @@ An error occurred trying to allocate memory. This should never happen.
[domains]: domain.md
[event emitter-based]: events.md#class-eventemitter
[file descriptors]: https://en.wikipedia.org/wiki/File_descriptor
[policy]: permissions.md#policies
[relative URL]: https://url.spec.whatwg.org/#relative-url-string
[self-reference a package using its name]: packages.md#self-referencing-a-package-using-its-name
[special scheme]: https://url.spec.whatwg.org/#special-scheme

View File

@ -1,11 +0,0 @@
# Policies
<!--introduced_in=v11.8.0-->
<!-- type=misc -->
> Stability: 1 - Experimental
The former Policies documentation is now at [Permissions documentation][].
[Permissions documentation]: permissions.md#policies

View File

@ -174,9 +174,6 @@ Enable experimental support for loading modules using `import` over `https:`.
.It Fl -experimental-permission
Enable the experimental permission model.
.
.It Fl -experimental-policy
Use the specified file as a security policy.
.
.It Fl -experimental-shadow-realm
Use this flag to enable ShadowRealm support.
.
@ -334,9 +331,6 @@ Among other uses, this can be used to enable FIPS-compliant crypto if Node.js is
.It Fl -pending-deprecation
Emit pending deprecation warnings.
.
.It Fl -policy-integrity Ns = Ns Ar sri
Instructs Node.js to error prior to running any code if the policy does not have the specified integrity. It expects a Subresource Integrity string as a parameter.
.
.It Fl -preserve-symlinks
Instructs the module loader to preserve symbolic links when resolving and caching modules other than the main module.
.

View File

@ -12,7 +12,6 @@
const {
AggregateError,
ArrayFrom,
ArrayIsArray,
ArrayPrototypeFilter,
ArrayPrototypeIncludes,
@ -1555,40 +1554,6 @@ E(
' `shortCircuit: true` in the hook\'s return.',
Error,
);
E('ERR_MANIFEST_ASSERT_INTEGRITY',
(moduleURL, realIntegrities) => {
let msg = `The content of "${
moduleURL
}" does not match the expected integrity.`;
if (realIntegrities.size) {
const sri = ArrayPrototypeJoin(
ArrayFrom(realIntegrities.entries(),
({ 0: alg, 1: dgs }) => `${alg}-${dgs}`),
' ',
);
msg += ` Integrities found are: ${sri}`;
} else {
msg += ' The resource was not found in the policy.';
}
return msg;
}, Error);
E('ERR_MANIFEST_DEPENDENCY_MISSING',
'Manifest resource %s does not list %s as a dependency specifier for ' +
'conditions: %s',
Error);
E('ERR_MANIFEST_INTEGRITY_MISMATCH',
'Manifest resource %s has multiple entries but integrity lists do not match',
SyntaxError);
E('ERR_MANIFEST_INVALID_RESOURCE_FIELD',
'Manifest resource %s has invalid property value for %s',
TypeError);
E('ERR_MANIFEST_INVALID_SPECIFIER',
'Manifest resource %s has invalid dependency mapping %s',
TypeError);
E('ERR_MANIFEST_TDZ', 'Manifest initialization has not yet run', Error);
E('ERR_MANIFEST_UNKNOWN_ONERROR',
'Manifest specified unknown error behavior "%s".',
SyntaxError);
E('ERR_METHOD_NOT_IMPLEMENTED', 'The %s method is not implemented', Error);
E('ERR_MISSING_ARGS',
(...args) => {

View File

@ -94,8 +94,6 @@ port.on('message', (message) => {
environmentData,
filename,
hasStdin,
manifestSrc,
manifestURL,
publicPort,
workerData,
} = message;
@ -130,9 +128,6 @@ port.on('message', (message) => {
workerIo.sharedCwdCounter = cwdCounter;
}
if (manifestSrc) {
require('internal/process/policy').setup(manifestSrc, manifestURL);
}
const isLoaderWorker =
doEval === 'internal' &&
filename === require('internal/modules/esm/utils').loaderWorkerId;

View File

@ -140,11 +140,6 @@ const fs = require('fs');
const path = require('path');
const { internalModuleStat } = internalBinding('fs');
const { safeGetenv } = internalBinding('credentials');
const {
privateSymbols: {
require_private_symbol,
},
} = internalBinding('util');
const {
getCjsConditions,
initializeCjsConditions,
@ -156,9 +151,6 @@ const {
} = require('internal/modules/helpers');
const packageJsonReader = require('internal/modules/package_json_reader');
const { getOptionValue, getEmbedderOptions } = require('internal/options');
const policy = getLazy(
() => (getOptionValue('--experimental-policy') ? require('internal/process/policy') : null),
);
const shouldReportRequiredModules = getLazy(() => process.env.WATCH_REPORT_DEPENDENCIES);
const permission = require('internal/process/permission');
@ -197,25 +189,6 @@ let requireDepth = 0;
let isPreloading = false;
let statCache = null;
/**
* Our internal implementation of `require`.
* @param {Module} module Parent module of what is being required
* @param {string} id Specifier of the child module being imported
*/
function internalRequire(module, id) {
validateString(id, 'id');
if (id === '') {
throw new ERR_INVALID_ARG_VALUE('id', id,
'must be a non-empty string');
}
requireDepth++;
try {
return Module._load(id, module, /* isMain */ false);
} finally {
requireDepth--;
}
}
/**
* Get a path's properties, using an in-memory cache to minimize lookups.
* @param {string} filename Absolute path to the file
@ -294,17 +267,6 @@ function Module(id = '', parent) {
this.filename = null;
this.loaded = false;
this.children = [];
let redirects;
const manifest = policy()?.manifest;
if (manifest) {
const moduleURL = pathToFileURL(id);
redirects = manifest.getDependencyMapper(moduleURL);
// TODO(rafaelgss): remove the necessity of this branch
setOwnProperty(this, 'require', makeRequireFunction(this, redirects));
// eslint-disable-next-line no-proto
setOwnProperty(this.__proto__, 'require', makeRequireFunction(this, redirects));
}
this[require_private_symbol] = internalRequire;
}
/** @type {Record<string, Module>} */
@ -1295,7 +1257,6 @@ Module.prototype.load = function(filename) {
/**
* Loads a module at the given file path. Returns that module's `exports` property.
* Note: when using the experimental policy mechanism this function is overridden.
* @param {string} id
* @throws {ERR_INVALID_ARG_TYPE} When `id` is not a string
*/
@ -1411,14 +1372,7 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) {
* @param {'module'|'commonjs'|undefined} format Intended format of the module.
*/
Module.prototype._compile = function(content, filename, format) {
let moduleURL;
let redirects;
const manifest = policy()?.manifest;
if (manifest) {
moduleURL = pathToFileURL(filename);
redirects = manifest.getDependencyMapper(moduleURL);
manifest.assertIntegrity(moduleURL, content);
}
let compiledWrapper;
if (format !== 'module') {
@ -1572,12 +1526,6 @@ Module._extensions['.js'] = function(module, filename) {
Module._extensions['.json'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');
const manifest = policy()?.manifest;
if (manifest) {
const moduleURL = pathToFileURL(filename);
manifest.assertIntegrity(moduleURL, content);
}
try {
setOwnProperty(module, 'exports', JSONParse(stripBOM(content)));
} catch (err) {
@ -1592,12 +1540,6 @@ Module._extensions['.json'] = function(module, filename) {
* @param {string} filename The file path of the module
*/
Module._extensions['.node'] = function(module, filename) {
const manifest = policy()?.manifest;
if (manifest) {
const content = fs.readFileSync(filename);
const moduleURL = pathToFileURL(filename);
manifest.assertIntegrity(moduleURL, content);
}
// Be aware this doesn't use `content`
return process.dlopen(module, path.toNamespacedPath(filename));
};
@ -1708,7 +1650,7 @@ Module._preloadModules = function(requests) {
}
}
for (let n = 0; n < requests.length; n++) {
internalRequire(parent, requests[n]);
parent.require(requests[n]);
}
isPreloading = false;
};
@ -1728,7 +1670,7 @@ Module.syncBuiltinESMExports = function syncBuiltinESMExports() {
ObjectDefineProperty(Module.prototype, 'constructor', {
__proto__: null,
get: function() {
return policy() ? undefined : Module;
return Module;
},
configurable: false,
enumerable: false,

View File

@ -12,10 +12,6 @@ const { validateAttributes, emitImportAssertionWarning } = require('internal/mod
const { getOptionValue } = require('internal/options');
const { readFileSync } = require('fs');
// Do not eagerly grab .manifest, it may be in TDZ
const policy = getOptionValue('--experimental-policy') ?
require('internal/process/policy') :
null;
const experimentalNetworkImports =
getOptionValue('--experimental-network-imports');
const defaultType =
@ -66,9 +62,6 @@ async function getSource(url, context) {
}
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url, supportedSchemes);
}
if (policy?.manifest) {
policy.manifest.assertIntegrity(href, source);
}
return { __proto__: null, responseURL, source };
}
@ -94,9 +87,6 @@ function getSourceSync(url, context) {
const supportedSchemes = ['file', 'data'];
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url, supportedSchemes);
}
if (policy?.manifest) {
policy.manifest.assertIntegrity(url, source);
}
return { __proto__: null, responseURL, source };
}

View File

@ -27,9 +27,6 @@ const { BuiltinModule } = require('internal/bootstrap/realm');
const { realpathSync } = 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 { sep, posix: { relative: relativePosixPath }, toNamespacedPath, resolve } = require('path');
const preserveSymlinks = getOptionValue('--preserve-symlinks');
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
@ -46,7 +43,6 @@ const {
ERR_INVALID_MODULE_SPECIFIER,
ERR_INVALID_PACKAGE_CONFIG,
ERR_INVALID_PACKAGE_TARGET,
ERR_MANIFEST_DEPENDENCY_MISSING,
ERR_MODULE_NOT_FOUND,
ERR_PACKAGE_IMPORT_NOT_DEFINED,
ERR_PACKAGE_PATH_NOT_EXPORTED,
@ -1045,8 +1041,7 @@ function throwIfInvalidParentURL(parentURL) {
/**
* Resolves the given specifier using the provided context, which includes the parent URL and conditions.
* Throws an error if the parent URL is invalid or if the resolution is disallowed by the policy manifest.
* Otherwise, attempts to resolve the specifier and returns the resulting URL and format.
* Attempts to resolve the specifier and returns the resulting URL and format.
* @param {string} specifier - The specifier to resolve.
* @param {object} [context={}] - The context object containing the parent URL and conditions.
* @param {string} [context.parentURL] - The URL of the parent module.
@ -1055,30 +1050,6 @@ function throwIfInvalidParentURL(parentURL) {
function defaultResolve(specifier, context = {}) {
let { parentURL, conditions } = context;
throwIfInvalidParentURL(parentURL);
if (parentURL && policy?.manifest) {
const redirects = policy.manifest.getDependencyMapper(parentURL);
if (redirects) {
const { resolve, reaction } = redirects;
const destination = resolve(specifier, new SafeSet(conditions));
let missing = true;
if (destination === true) {
missing = false;
} else if (destination) {
const href = destination.href;
return { __proto__: null, url: href };
}
if (missing) {
// Prevent network requests from firing if resolution would be banned.
// Network requests can extract data by doing things like putting
// secrets in query params
reaction(new ERR_MANIFEST_DEPENDENCY_MISSING(
parentURL,
specifier,
ArrayPrototypeJoin([...conditions], ', ')),
);
}
}
}
let parsedParentURL;
if (parentURL) {
@ -1207,22 +1178,3 @@ module.exports = {
const {
defaultGetFormatWithoutErrors,
} = require('internal/modules/esm/get_format');
if (policy) {
const $defaultResolve = defaultResolve;
module.exports.defaultResolve = function defaultResolve(
specifier,
context,
) {
const ret = $defaultResolve(specifier, context);
// 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;
};
}

View File

@ -2,7 +2,6 @@
const {
ArrayPrototypeForEach,
ArrayPrototypeJoin,
ObjectDefineProperty,
ObjectPrototypeHasOwnProperty,
SafeMap,
@ -14,8 +13,6 @@ const {
} = primordials;
const {
ERR_INVALID_ARG_TYPE,
ERR_MANIFEST_DEPENDENCY_MISSING,
ERR_UNKNOWN_BUILTIN_MODULE,
} = require('internal/errors').codes;
const { BuiltinModule } = require('internal/bootstrap/realm');
@ -30,11 +27,6 @@ const { getOptionValue } = require('internal/options');
const { setOwnProperty } = require('internal/util');
const { inspect } = require('internal/util/inspect');
const {
privateSymbols: {
require_private_symbol,
},
} = internalBinding('util');
const { canParse: URLCanParse } = internalBinding('url');
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
@ -115,69 +107,20 @@ function lazyModule() {
return $Module;
}
/**
* Invoke with `makeRequireFunction(module)` where `module` is the `Module` object to use as the context for the
* `require()` function.
* Use redirects to set up a mapping from a policy and restrict dependencies.
*/
const urlToFileCache = new SafeMap();
/**
* Create the module-scoped `require` function to pass into CommonJS modules.
* @param {Module} mod - The module to create the `require` function for.
* @param {ReturnType<import('internal/policy/manifest.js').Manifest['getDependencyMapper']>} redirects
* @typedef {(specifier: string) => unknown} RequireFunction
*/
function makeRequireFunction(mod, redirects) {
function makeRequireFunction(mod) {
// lazy due to cycle
const Module = lazyModule();
if (mod instanceof Module !== true) {
throw new ERR_INVALID_ARG_TYPE('mod', 'Module', mod);
}
/** @type {RequireFunction} */
let require;
if (redirects) {
const id = mod.filename || mod.id;
const conditions = getCjsConditions();
const { resolve, reaction } = redirects;
require = function require(specifier) {
let missing = true;
const destination = resolve(specifier, conditions);
if (destination === true) {
missing = false;
} else if (destination) {
const { href, protocol } = destination;
if (protocol === 'node:') {
const specifier = destination.pathname;
if (BuiltinModule.canBeRequiredByUsers(specifier)) {
const mod = loadBuiltinModule(specifier, href);
return mod.exports;
}
throw new ERR_UNKNOWN_BUILTIN_MODULE(specifier);
} else if (protocol === 'file:') {
let filepath = urlToFileCache.get(href);
if (!filepath) {
filepath = fileURLToPath(destination);
urlToFileCache.set(href, filepath);
}
return mod[require_private_symbol](mod, filepath);
}
}
if (missing) {
reaction(new ERR_MANIFEST_DEPENDENCY_MISSING(
id,
specifier,
ArrayPrototypeJoin([...conditions], ', '),
));
}
return mod[require_private_symbol](mod, specifier);
};
} else {
require = function require(path) {
// When no policy manifest, the original prototype.require is sustained
return mod.require(path);
};
function require(path) {
return mod.require(path);
}
/**

View File

@ -10,34 +10,13 @@ const {
const modulesBinding = internalBinding('modules');
const { resolve, sep } = require('path');
const { kEmptyObject } = require('internal/util');
const { pathToFileURL } = require('internal/url');
let manifest;
/**
* @param {string} jsonPath
* @param {string} value The integrity value to check against.
*/
function checkPackageJSONIntegrity(jsonPath, value) {
if (manifest === undefined) {
const { getOptionValue } = require('internal/options');
manifest = getOptionValue('--experimental-policy') ?
require('internal/process/policy').manifest :
null;
}
if (manifest !== null) {
const jsonURL = pathToFileURL(jsonPath);
manifest.assertIntegrity(jsonURL, value);
}
}
/**
* @param {string} path
* @param {import('typings/internalBinding/modules').SerializedPackageConfig} contents
* @param {boolean} [checkIntegrity=false] Whether to check the integrity of the package.json file.
* @returns {import('typings/internalBinding/modules').PackageConfig}
*/
function deserializePackageJSON(path, contents, checkIntegrity = false) {
function deserializePackageJSON(path, contents) {
if (contents === undefined) {
return {
__proto__: null,
@ -54,8 +33,7 @@ function deserializePackageJSON(path, contents, checkIntegrity = false) {
2: type,
3: plainImports,
4: plainExports,
5: manifest,
6: optionalFilePath,
5: optionalFilePath,
} = contents;
// This is required to be used in getPackageScopeConfig.
@ -63,11 +41,6 @@ function deserializePackageJSON(path, contents, checkIntegrity = false) {
pjsonPath = optionalFilePath;
}
if (checkIntegrity) {
// parsed[5] is only available when experimental policy is enabled.
checkPackageJSONIntegrity(pjsonPath, manifest);
}
// The imports and exports fields can be either undefined or a string.
// - If it's a string, it's either plain string or a stringified JSON string.
// - If it's a stringified JSON string, it starts with either '[' or '{'.
@ -114,7 +87,7 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
specifier == null ? undefined : `${specifier}`,
);
return deserializePackageJSON(jsonPath, parsed, true /* checkIntegrity */);
return deserializePackageJSON(jsonPath, parsed);
}
/**
@ -141,7 +114,7 @@ function getNearestParentPackageJSON(checkPath) {
return undefined;
}
const data = deserializePackageJSON(checkPath, result, true /* checkIntegrity */);
const data = deserializePackageJSON(checkPath, result);
// Path should be the root folder of the matched package.json
// For example for ~/path/package.json, it should be ~/path
@ -159,7 +132,7 @@ function getPackageScopeConfig(resolved) {
const result = modulesBinding.getPackageScopeConfig(`${resolved}`);
if (ArrayIsArray(result)) {
return deserializePackageJSON(`${resolved}`, result, false /* checkIntegrity */);
return deserializePackageJSON(`${resolved}`, result);
}
// This means that the response is a string
@ -182,7 +155,6 @@ function getPackageType(url) {
}
module.exports = {
checkPackageJSONIntegrity,
read,
readPackage,
getNearestParentPackageJSON,

View File

@ -7,7 +7,6 @@ const {
const { getNearestParentPackageJSONType } = internalBinding('modules');
const { getOptionValue } = require('internal/options');
const { checkPackageJSONIntegrity } = require('internal/modules/package_json_reader');
const path = require('path');
const { pathToFileURL } = require('internal/url');
const { kEmptyObject, getCWDURL } = require('internal/util');
@ -82,22 +81,13 @@ function shouldUseESMLoader(mainPath) {
if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) { return true; }
if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) { return false; }
const response = getNearestParentPackageJSONType(mainPath);
const type = getNearestParentPackageJSONType(mainPath);
// No package.json or no `type` field.
if (response === undefined || response[0] === 'none') {
if (type === undefined || type === 'none') {
return false;
}
// TODO(@anonrig): Do not return filePath and rawContent if experimental-policy is not used.
const {
0: type,
1: filePath,
2: rawContent,
} = response;
checkPackageJSONIntegrity(filePath, rawContent);
return type === 'module';
}

View File

@ -1,751 +0,0 @@
'use strict';
// #region imports
const {
ArrayIsArray,
ArrayPrototypeSort,
ObjectEntries,
ObjectFreeze,
ObjectKeys,
ObjectSetPrototypeOf,
RegExpPrototypeExec,
RegExpPrototypeSymbolReplace,
SafeMap,
SafeSet,
StringPrototypeEndsWith,
StringPrototypeStartsWith,
Symbol,
} = primordials;
const {
ERR_MANIFEST_ASSERT_INTEGRITY,
ERR_MANIFEST_INVALID_RESOURCE_FIELD,
ERR_MANIFEST_INVALID_SPECIFIER,
ERR_MANIFEST_UNKNOWN_ONERROR,
} = require('internal/errors').codes;
let debug = require('internal/util/debuglog').debuglog('policy', (fn) => {
debug = fn;
});
const SRI = require('internal/policy/sri');
const { URL } = require('internal/url');
const { internalVerifyIntegrity } = internalBinding('crypto');
const kRelativeURLStringPattern = /^\.{0,2}\//;
const { getOptionValue } = require('internal/options');
const shouldAbortOnUncaughtException = getOptionValue(
'--abort-on-uncaught-exception',
);
const { exitCodes: { kGenericUserError } } = internalBinding('errors');
const { abort, exit, _rawDebug } = process;
// #endregion
// #region constants
// From https://url.spec.whatwg.org/#special-scheme
const kSpecialSchemes = new SafeSet([
'file:',
'ftp:',
'http:',
'https:',
'ws:',
'wss:',
]);
/**
* @type {symbol}
*/
const kCascade = Symbol('cascade');
/**
* @type {symbol}
*/
const kFallThrough = Symbol('fall through');
function REACTION_THROW(error) {
throw error;
}
function REACTION_EXIT(error) {
REACTION_LOG(error);
if (shouldAbortOnUncaughtException) {
abort();
}
exit(kGenericUserError);
}
function REACTION_LOG(error) {
_rawDebug(error.stack);
}
// #endregion
// #region DependencyMapperInstance
class DependencyMapperInstance {
/**
* @type {string}
*/
href;
/**
* @type {DependencyMap | undefined}
*/
#dependencies;
/**
* @type {PatternDependencyMap | undefined}
*/
#patternDependencies;
/**
* @type {DependencyMapperInstance | null | undefined}
*/
#parentDependencyMapper;
/**
* @type {boolean}
*/
#normalized = false;
/**
* @type {boolean}
*/
cascade;
/**
* @type {boolean}
*/
allowSameHREFScope;
/**
* @param {string} parentHREF
* @param {DependencyMap | undefined} dependencies
* @param {boolean} cascade
* @param {boolean} allowSameHREFScope
*/
constructor(
parentHREF,
dependencies,
cascade = false,
allowSameHREFScope = false) {
this.href = parentHREF;
if (dependencies === kFallThrough ||
dependencies === undefined ||
dependencies === null) {
this.#dependencies = dependencies;
this.#patternDependencies = undefined;
} else {
const patterns = [];
const keys = ObjectKeys(dependencies);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (StringPrototypeEndsWith(key, '*')) {
const target = RegExpPrototypeExec(/^([^*]*)\*([^*]*)$/);
if (!target) {
throw new ERR_MANIFEST_INVALID_SPECIFIER(
this.href,
`${target}, pattern needs to have a single trailing "*" in target`,
);
}
const prefix = target[1];
const suffix = target[2];
patterns.push([
target.slice(0, -1),
[prefix, suffix],
]);
}
}
ArrayPrototypeSort(patterns, (a, b) => {
return a[0] < b[0] ? -1 : 1;
});
this.#dependencies = dependencies;
this.#patternDependencies = patterns;
}
this.cascade = cascade;
this.allowSameHREFScope = allowSameHREFScope;
ObjectFreeze(this);
}
/**
*
* @param {string} normalizedSpecifier
* @param {Set<string>} conditions
* @param {Manifest} manifest
* @returns {URL | typeof kFallThrough | null}
*/
_resolveAlreadyNormalized(normalizedSpecifier, conditions, manifest) {
let dependencies = this.#dependencies;
debug(this.href, 'resolving', normalizedSpecifier);
if (dependencies === kFallThrough) return true;
if (dependencies !== undefined && typeof dependencies === 'object') {
const normalized = this.#normalized;
if (normalized !== true) {
/**
* @type {Record<string, string>}
*/
const normalizedDependencyMap = { __proto__: null };
for (let specifier in dependencies) {
const target = dependencies[specifier];
specifier = canonicalizeSpecifier(specifier, manifest.href);
normalizedDependencyMap[specifier] = target;
}
ObjectFreeze(normalizedDependencyMap);
dependencies = normalizedDependencyMap;
this.#dependencies = normalizedDependencyMap;
this.#normalized = true;
}
debug(dependencies);
if (normalizedSpecifier in dependencies === true) {
const to = searchDependencies(
this.href,
dependencies[normalizedSpecifier],
conditions,
);
debug({ to });
if (to === true) {
return true;
}
let ret;
if (parsedURLs && parsedURLs.has(to)) {
ret = parsedURLs.get(to);
} else if (RegExpPrototypeExec(kRelativeURLStringPattern, to) !== null) {
ret = resolve(to, manifest.href);
} else {
ret = resolve(to);
}
return ret;
}
}
const { cascade } = this;
if (cascade !== true) {
return null;
}
let parentDependencyMapper = this.#parentDependencyMapper;
if (parentDependencyMapper === undefined) {
parentDependencyMapper = manifest.getScopeDependencyMapper(
this.href,
this.allowSameHREFScope,
);
this.#parentDependencyMapper = parentDependencyMapper;
}
if (parentDependencyMapper === null) {
return null;
}
return parentDependencyMapper._resolveAlreadyNormalized(
normalizedSpecifier,
conditions,
manifest,
);
}
}
const kArbitraryDependencies = new DependencyMapperInstance(
'arbitrary dependencies',
kFallThrough,
false,
true,
);
const kNoDependencies = new DependencyMapperInstance(
'no dependencies',
null,
false,
true,
);
/**
* @param {string} href
* @param {JSONDependencyMap} dependencies
* @param {boolean} cascade
* @param {boolean} allowSameHREFScope
* @param {Map<string | null | undefined, DependencyMapperInstance>} store
*/
const insertDependencyMap = (
href,
dependencies,
cascade,
allowSameHREFScope,
store,
) => {
if (cascade !== undefined && typeof cascade !== 'boolean') {
throw new ERR_MANIFEST_INVALID_RESOURCE_FIELD(href, 'cascade');
}
if (dependencies === true) {
store.set(href, kArbitraryDependencies);
return;
}
if (dependencies === null || dependencies === undefined) {
store.set(
href,
cascade ?
new DependencyMapperInstance(href, null, true, allowSameHREFScope) :
kNoDependencies,
);
return;
}
if (objectButNotArray(dependencies)) {
store.set(
href,
new DependencyMapperInstance(
href,
dependencies,
cascade,
allowSameHREFScope,
),
);
return;
}
throw new ERR_MANIFEST_INVALID_RESOURCE_FIELD(href, 'dependencies');
};
/**
* Finds the longest key within `this.#scopeDependencies` that covers a
* specific HREF
* @param {string} href
* @param {ScopeStore} scopeStore
* @returns {null | string}
*/
function findScopeHREF(href, scopeStore, allowSame) {
let protocol;
if (href !== '') {
// default URL parser does some stuff to special urls... skip if this is
// just the protocol
if (RegExpPrototypeExec(/^[^:]*[:]$/, href) !== null) {
protocol = href;
} else {
let currentURL = new URL(href);
const normalizedHREF = currentURL.href;
protocol = currentURL.protocol;
// Non-opaque blobs adopt origins
if (protocol === 'blob:' && currentURL.origin !== 'null') {
currentURL = new URL(currentURL.origin);
protocol = currentURL.protocol;
}
// Only a few schemes are hierarchical
if (kSpecialSchemes.has(currentURL.protocol)) {
// Make first '..' act like '.'
if (!StringPrototypeEndsWith(currentURL.pathname, '/')) {
currentURL.pathname += '/';
}
let lastHREF;
let currentHREF = currentURL.href;
do {
if (scopeStore.has(currentHREF)) {
if (allowSame || currentHREF !== normalizedHREF) {
return currentHREF;
}
}
lastHREF = currentHREF;
currentURL = new URL('..', currentURL);
currentHREF = currentURL.href;
} while (lastHREF !== currentHREF);
}
}
}
if (scopeStore.has(protocol)) {
if (allowSame || protocol !== href) return protocol;
}
if (scopeStore.has('')) {
if (allowSame || '' !== href) return '';
}
return null;
}
// #endregion
/**
* @typedef {Record<string, string> | typeof kFallThrough} DependencyMap
* @typedef {Array<[string, [string, string]]>} PatternDependencyMap
* @typedef {Record<string, string> | null | true} JSONDependencyMap
*/
/**
* @typedef {Map<string, any>} ScopeStore
* @typedef {(specifier: string) => true | URL} DependencyMapper
* @typedef {boolean | string | SRI[] | typeof kCascade} Integrity
*/
class Manifest {
#defaultDependencies;
/**
* @type {string}
*/
href;
/**
* @type {(err: Error) => void}
*
* Performs default action for what happens when a manifest encounters
* a violation such as abort()ing or exiting the process, throwing the error,
* or logging the error.
*/
#reaction;
/**
* @type {Map<string, DependencyMapperInstance>}
*
* Used to find where a dependency is located.
*
* This stores functions to lazily calculate locations as needed.
* `true` is used to signify that the location is not specified
* by the manifest and default resolution should be allowed.
*
* The functions return `null` to signify that a dependency is
* not found
*/
#resourceDependencies = new SafeMap();
/**
* @type {Map<string, Integrity>}
*
* Used to compare a resource to the content body at the resource.
* `true` is used to signify that all integrities are allowed, otherwise,
* SRI strings are parsed to compare with the body.
*
* This stores strings instead of eagerly parsing SRI strings
* and only converts them to SRI data structures when needed.
* This avoids needing to parse all SRI strings at startup even
* if some never end up being used.
*/
#resourceIntegrities = new SafeMap();
/**
* @type {ScopeStore}
*
* Used to compare a resource to the content body at the resource.
* `true` is used to signify that all integrities are allowed, otherwise,
* SRI strings are parsed to compare with the body.
*
* Separate from #resourceDependencies due to conflicts with things like
* `blob:` being both a scope and a resource potentially as well as
* `file:` being parsed to `file:///` instead of remaining host neutral.
*/
#scopeDependencies = new SafeMap();
/**
* @type {Map<string, boolean | null | typeof kCascade>}
*
* Used to allow arbitrary loading within a scope
*/
#scopeIntegrities = new SafeMap();
/**
* `obj` should match the policy file format described in the docs
* it is expected to not have prototype pollution issues either by reassigning
* the prototype to `null` for values or by running prior to any user code.
*
* `manifestURL` is a URL to resolve relative locations against.
* @param {object} obj
* @param {string} manifestHREF
*/
constructor(obj, manifestHREF) {
this.href = manifestHREF;
const scopes = this.#scopeDependencies;
const integrities = this.#resourceIntegrities;
const resourceDependencies = this.#resourceDependencies;
let reaction = REACTION_THROW;
if (objectButNotArray(obj) && 'onerror' in obj) {
const behavior = obj.onerror;
if (behavior === 'exit') {
reaction = REACTION_EXIT;
} else if (behavior === 'log') {
reaction = REACTION_LOG;
} else if (behavior !== 'throw') {
throw new ERR_MANIFEST_UNKNOWN_ONERROR(behavior);
}
}
this.#reaction = reaction;
const jsonResourcesEntries = ObjectEntries(
obj.resources ?? { __proto__: null },
);
const jsonScopesEntries = ObjectEntries(obj.scopes ?? { __proto__: null });
const defaultDependencies = obj.dependencies ?? { __proto__: null };
this.#defaultDependencies = new DependencyMapperInstance(
'default',
defaultDependencies === true ? kFallThrough : defaultDependencies,
false,
);
for (let i = 0; i < jsonResourcesEntries.length; i++) {
const { 0: originalHREF, 1: descriptor } = jsonResourcesEntries[i];
const { cascade, dependencies, integrity } = descriptor;
const href = resolve(originalHREF, manifestHREF).href;
if (typeof integrity !== 'undefined') {
debug('Manifest contains integrity for resource %s', originalHREF);
if (typeof integrity === 'string') {
integrities.set(href, integrity);
} else if (integrity === true) {
integrities.set(href, true);
} else {
throw new ERR_MANIFEST_INVALID_RESOURCE_FIELD(href, 'integrity');
}
} else {
integrities.set(href, cascade === true ? kCascade : false);
}
insertDependencyMap(
href,
dependencies,
cascade,
true,
resourceDependencies,
);
}
const scopeIntegrities = this.#scopeIntegrities;
for (let i = 0; i < jsonScopesEntries.length; i++) {
const { 0: originalHREF, 1: descriptor } = jsonScopesEntries[i];
const { cascade, dependencies, integrity } = descriptor;
const href = emptyOrProtocolOrResolve(originalHREF, manifestHREF);
if (typeof integrity !== 'undefined') {
debug('Manifest contains integrity for scope %s', originalHREF);
if (integrity === true) {
scopeIntegrities.set(href, true);
} else {
throw new ERR_MANIFEST_INVALID_RESOURCE_FIELD(href, 'integrity');
}
} else {
scopeIntegrities.set(href, cascade === true ? kCascade : false);
}
insertDependencyMap(href, dependencies, cascade, false, scopes);
}
ObjectFreeze(this);
}
/**
* @param {string} requester
* @returns {{resolve: any, reaction: (err: any) => void}}
*/
getDependencyMapper(requester) {
const requesterHREF = `${requester}`;
const dependencies = this.#resourceDependencies;
/**
* @type {DependencyMapperInstance}
*/
const instance = (
dependencies.has(requesterHREF) ?
dependencies.get(requesterHREF) ?? null :
this.getScopeDependencyMapper(requesterHREF, true)
) ?? this.#defaultDependencies;
return {
resolve: (specifier, conditions) => {
const normalizedSpecifier = canonicalizeSpecifier(
specifier,
requesterHREF,
);
const result = instance._resolveAlreadyNormalized(
normalizedSpecifier,
conditions,
this,
);
if (result === kFallThrough) return true;
return result;
},
reaction: this.#reaction,
};
}
mightAllow(url, onreact) {
const href = `${url}`;
debug('Checking for entry of %s', href);
if (StringPrototypeStartsWith(href, 'node:')) {
return true;
}
if (this.#resourceIntegrities.has(href)) {
return true;
}
let scope = findScopeHREF(href, this.#scopeIntegrities, true);
while (scope !== null) {
if (this.#scopeIntegrities.has(scope)) {
const entry = this.#scopeIntegrities.get(scope);
if (entry === true) {
return true;
} else if (entry !== kCascade) {
break;
}
}
const nextScope = findScopeHREF(
new URL('..', scope),
this.#scopeIntegrities,
false,
);
if (!nextScope || nextScope === scope) {
break;
}
scope = nextScope;
}
if (onreact) {
this.#reaction(onreact());
}
return false;
}
assertIntegrity(url, content) {
const href = `${url}`;
debug('Checking integrity of %s', href);
const realIntegrities = new SafeMap();
const integrities = this.#resourceIntegrities;
function processEntry(href) {
let integrityEntries = integrities.get(href);
if (integrityEntries === true) return true;
if (typeof integrityEntries === 'string') {
const sri = ObjectFreeze(SRI.parse(integrityEntries));
integrities.set(href, sri);
integrityEntries = sri;
}
return integrityEntries;
}
if (integrities.has(href)) {
const integrityEntries = processEntry(href);
if (integrityEntries === true) return true;
if (ArrayIsArray(integrityEntries)) {
// Avoid clobbered Symbol.iterator
for (let i = 0; i < integrityEntries.length; i++) {
const { algorithm, value: expected } = integrityEntries[i];
// TODO(tniessen): the content should not be passed as a string in the
// first place, see https://github.com/nodejs/node/issues/39707
const mismatchedIntegrity = internalVerifyIntegrity(algorithm, content, expected);
if (mismatchedIntegrity === undefined) {
return true;
}
realIntegrities.set(algorithm, mismatchedIntegrity);
}
}
if (integrityEntries !== kCascade) {
const error = new ERR_MANIFEST_ASSERT_INTEGRITY(url, realIntegrities);
this.#reaction(error);
}
}
let scope = findScopeHREF(href, this.#scopeIntegrities, true);
while (scope !== null) {
if (this.#scopeIntegrities.has(scope)) {
const entry = this.#scopeIntegrities.get(scope);
if (entry === true) {
return true;
} else if (entry !== kCascade) {
break;
}
}
const nextScope = findScopeHREF(scope, this.#scopeDependencies, false);
if (!nextScope) {
break;
}
scope = nextScope;
}
const error = new ERR_MANIFEST_ASSERT_INTEGRITY(url, realIntegrities);
this.#reaction(error);
}
/**
* @param {string} href
* @param {boolean} allowSameHREFScope
* @returns {DependencyMapperInstance | null}
*/
getScopeDependencyMapper(href, allowSameHREFScope) {
if (href === null) {
return this.#defaultDependencies;
}
/** @type {string | null} */
const scopeHREF = findScopeHREF(
href,
this.#scopeDependencies,
allowSameHREFScope,
);
if (scopeHREF === null) return this.#defaultDependencies;
return this.#scopeDependencies.get(scopeHREF);
}
}
// Lock everything down to avoid problems even if reference is leaked somehow
ObjectSetPrototypeOf(Manifest, null);
ObjectSetPrototypeOf(Manifest.prototype, null);
ObjectFreeze(Manifest);
ObjectFreeze(Manifest.prototype);
module.exports = ObjectFreeze({ Manifest });
// #region URL utils
/**
* Attempts to canonicalize relative URL strings against a base URL string
* Does not perform I/O
* If not able to canonicalize, returns the original specifier
*
* This effectively removes the possibility of the return value being a relative
* URL string
* @param {string} specifier
* @param {string} base
* @returns {string}
*/
function canonicalizeSpecifier(specifier, base) {
try {
if (RegExpPrototypeExec(kRelativeURLStringPattern, specifier) !== null) {
return resolve(specifier, base).href;
}
return resolve(specifier).href;
} catch {
// Continue regardless of error.
}
return specifier;
}
/**
* Does a special allowance for scopes to be non-valid URLs
* that are only protocol strings or the empty string
* @param {string} resourceHREF
* @param {string} [base]
* @returns {string}
*/
const emptyOrProtocolOrResolve = (resourceHREF, base) => {
if (resourceHREF === '') return '';
if (StringPrototypeEndsWith(resourceHREF, ':')) {
// URL parse will trim these anyway, save the compute
resourceHREF = RegExpPrototypeSymbolReplace(
// eslint-disable-next-line
/^[\x00-\x1F\x20]|\x09\x0A\x0D|[\x00-\x1F\x20]$/g,
resourceHREF,
'',
);
if (RegExpPrototypeExec(/^[a-zA-Z][a-zA-Z+\-.]*:$/, resourceHREF) !== null) {
return resourceHREF;
}
}
return resolve(resourceHREF, base).href;
};
/**
* @type {Map<string, URL>}
*/
let parsedURLs;
/**
* Resolves a valid url string and uses the parsed cache to avoid double parsing
* costs.
* @param {string} originalHREF
* @param {string} [base]
* @returns {Readonly<URL>}
*/
const resolve = (originalHREF, base) => {
parsedURLs = parsedURLs ?? new SafeMap();
if (parsedURLs.has(originalHREF)) {
return parsedURLs.get(originalHREF);
} else if (RegExpPrototypeExec(kRelativeURLStringPattern, originalHREF) !== null) {
const resourceURL = new URL(originalHREF, base);
parsedURLs.set(resourceURL.href, resourceURL);
return resourceURL;
}
const resourceURL = new URL(originalHREF);
parsedURLs.set(originalHREF, resourceURL);
return resourceURL;
};
// #endregion
/**
* @param {any} o
* @returns {o is object}
*/
function objectButNotArray(o) {
return o && typeof o === 'object' && !ArrayIsArray(o);
}
function searchDependencies(href, target, conditions) {
if (objectButNotArray(target)) {
const keys = ObjectKeys(target);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (conditions.has(key)) {
const ret = searchDependencies(href, target[key], conditions);
if (ret != null) {
return ret;
}
}
}
} else if (typeof target === 'string') {
return target;
} else if (target === true) {
return target;
} else {
throw new ERR_MANIFEST_INVALID_RESOURCE_FIELD(href, 'dependencies');
}
return null;
}

View File

@ -1,73 +0,0 @@
'use strict';
// Utility to parse the value of
// https://w3c.github.io/webappsec-subresource-integrity/#the-integrity-attribute
const {
ArrayPrototype,
ObjectDefineProperty,
ObjectFreeze,
ObjectSeal,
ObjectSetPrototypeOf,
RegExp,
RegExpPrototypeExec,
StringPrototypeSlice,
} = primordials;
const {
ERR_SRI_PARSE,
} = require('internal/errors').codes;
const kWSP = '[\\x20\\x09]';
const kVCHAR = '[\\x21-\\x7E]';
const kHASH_ALGO = 'sha(?:256|384|512)';
// Base64
const kHASH_VALUE = '[A-Za-z0-9+/]+[=]{0,2}';
const kHASH_EXPRESSION = `(${kHASH_ALGO})-(${kHASH_VALUE})`;
// Ungrouped since unused
const kOPTION_EXPRESSION = `(?:${kVCHAR}*)`;
const kHASH_WITH_OPTIONS = `${kHASH_EXPRESSION}(?:[?](${kOPTION_EXPRESSION}))?`;
const kSRIPattern = RegExp(`(${kWSP}*)(?:${kHASH_WITH_OPTIONS})`, 'g');
ObjectSeal(kSRIPattern);
const kAllWSP = RegExp(`^${kWSP}*$`);
ObjectSeal(kAllWSP);
const BufferFrom = require('buffer').Buffer.from;
// Returns {algorithm, value (in base64 string), options,}[]
const parse = (str) => {
let prevIndex = 0;
let match;
const entries = [];
while ((match = RegExpPrototypeExec(kSRIPattern, str)) !== null) {
if (match.index !== prevIndex) {
throw new ERR_SRI_PARSE(str, str[prevIndex], prevIndex);
}
if (entries.length > 0 && match[1] === '') {
throw new ERR_SRI_PARSE(str, str[prevIndex], prevIndex);
}
// Avoid setters being fired
ObjectDefineProperty(entries, entries.length, {
__proto__: null,
enumerable: true,
configurable: true,
value: ObjectFreeze({
__proto__: null,
algorithm: match[2],
value: BufferFrom(match[3], 'base64'),
options: match[4] === undefined ? null : match[4],
}),
});
prevIndex += match[0].length;
}
if (prevIndex !== str.length) {
if (RegExpPrototypeExec(kAllWSP, StringPrototypeSlice(str, prevIndex)) === null) {
throw new ERR_SRI_PARSE(str, str[prevIndex], prevIndex);
}
}
return ObjectSetPrototypeOf(entries, ArrayPrototype);
};
module.exports = {
parse,
};

View File

@ -1,71 +0,0 @@
'use strict';
const {
JSONParse,
ObjectFreeze,
ReflectSetPrototypeOf,
} = primordials;
const {
ERR_ACCESS_DENIED,
ERR_MANIFEST_TDZ,
} = require('internal/errors').codes;
const { Manifest } = require('internal/policy/manifest');
let manifest;
let manifestSrc;
let manifestURL;
module.exports = ObjectFreeze({
__proto__: null,
setup(src, url) {
manifestSrc = src;
manifestURL = url;
if (src === null) {
manifest = null;
return;
}
const json = JSONParse(src, (_, o) => {
if (o && typeof o === 'object') {
ReflectSetPrototypeOf(o, null);
ObjectFreeze(o);
}
return o;
});
manifest = new Manifest(json, url);
// process.binding() is deprecated (DEP0111) and trivially allows bypassing
// policies, so if policies are enabled, make this API unavailable.
process.binding = function binding(_module) {
throw new ERR_ACCESS_DENIED('process.binding');
};
process._linkedBinding = function _linkedBinding(_module) {
throw new ERR_ACCESS_DENIED('process._linkedBinding');
};
},
get manifest() {
if (typeof manifest === 'undefined') {
throw new ERR_MANIFEST_TDZ();
}
return manifest;
},
get src() {
if (typeof manifestSrc === 'undefined') {
throw new ERR_MANIFEST_TDZ();
}
return manifestSrc;
},
get url() {
if (typeof manifestURL === 'undefined') {
throw new ERR_MANIFEST_TDZ();
}
return manifestURL;
},
assertIntegrity(moduleURL, content) {
this.manifest.assertIntegrity(moduleURL, content);
},
});

View File

@ -12,7 +12,6 @@ const {
NumberParseInt,
ObjectDefineProperty,
ObjectFreeze,
SafeMap,
String,
StringPrototypeStartsWith,
Symbol,
@ -34,7 +33,6 @@ const {
} = require('internal/util');
const {
ERR_MANIFEST_ASSERT_INTEGRITY,
ERR_MISSING_OPTION,
ERR_ACCESS_DENIED,
} = require('internal/errors').codes;
@ -57,7 +55,7 @@ function prepareMainThreadExecution(expandArgv1 = false, initializeModules = tru
function prepareWorkerThreadExecution() {
prepareExecution({
expandArgv1: false,
initializeModules: false, // Will need to initialize it after policy setup
initializeModules: false,
isMainThread: false,
});
}
@ -121,11 +119,6 @@ function prepareExecution(options) {
if (isMainThread) {
assert(internalBinding('worker').isMainThread);
// Worker threads will get the manifest in the message handler.
const policy = readPolicyFromDisk();
if (policy) {
require('internal/process/policy')
.setup(policy.manifestSrc, policy.manifestURL);
}
// Print stack trace on `SIGINT` if option `--trace-sigint` presents.
setupStacktracePrinterOnSigint();
@ -580,56 +573,6 @@ function initializePermission() {
}
}
function readPolicyFromDisk() {
const experimentalPolicy = getOptionValue('--experimental-policy');
if (experimentalPolicy) {
process.emitWarning('Policies are experimental.',
'ExperimentalWarning');
const { pathToFileURL, URL } = require('internal/url');
// URL here as it is slightly different parsing
// no bare specifiers for now
let manifestURL;
if (require('path').isAbsolute(experimentalPolicy)) {
manifestURL = pathToFileURL(experimentalPolicy);
} else {
const cwdURL = pathToFileURL(process.cwd());
cwdURL.pathname += '/';
manifestURL = new URL(experimentalPolicy, cwdURL);
}
const fs = require('fs');
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);
let foundMatch = false;
for (let i = 0; i < integrityEntries.length; i++) {
const {
algorithm,
value: expected,
} = integrityEntries[i];
const hash = createHash(algorithm);
hash.update(src);
const digest = hash.digest();
if (digest.length === expected.length &&
timingSafeEqual(digest, expected)) {
foundMatch = true;
break;
}
realIntegrities.set(algorithm, digest.toString('base64'));
}
if (!foundMatch) {
throw new ERR_MANIFEST_ASSERT_INTEGRITY(manifestURL, realIntegrities);
}
}
return {
manifestSrc: src, manifestURL: manifestURL.href,
};
}
}
function initializeCJSLoader() {
const { initializeCJS } = require('internal/modules/cjs/loader');
initializeCJS();

View File

@ -41,7 +41,6 @@ const {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
} = errorCodes;
const { getOptionValue } = require('internal/options');
const workerIo = require('internal/worker/io');
const {
@ -273,12 +272,6 @@ 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,
hasStdin: !!options.stdin,
}, transferList);
// Use this to cache the Worker's loopStart value once available.

View File

@ -1096,12 +1096,6 @@ void Environment::InitializeCompileCache() {
dir_from_env.empty()) {
return;
}
if (!options()->experimental_policy.empty()) {
Debug(this,
DebugCategory::COMPILE_CACHE,
"[compile cache] skipping cache because policy is enabled");
return;
}
auto handler = std::make_unique<CompileCacheHandler>(this);
if (handler->InitializeDirectory(this, dir_from_env)) {
compile_cache_handler_ = std::move(handler);

View File

@ -35,8 +35,7 @@
V(napi_wrapper, "node:napi:wrapper") \
V(untransferable_object_private_symbol, "node:untransferableObject") \
V(exit_info_private_symbol, "node:exit_info_private_symbol") \
V(promise_trace_id, "node:promise_trace_id") \
V(require_private_symbol, "node:require_private_symbol")
V(promise_trace_id, "node:promise_trace_id")
// Symbols are per-isolate primitives but Environment proxies them
// for the sake of convenience.

View File

@ -124,7 +124,6 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const {
"_tls_wrap", "internal/tls/secure-pair",
"internal/tls/parse-cert-string", "internal/tls/secure-context",
"internal/http2/core", "internal/http2/compat",
"internal/policy/manifest", "internal/process/policy",
"internal/streams/lazy_transform",
#endif // !HAVE_OPENSSL
"sys", // Deprecated.

View File

@ -76,23 +76,21 @@ void BindingData::Deserialize(v8::Local<v8::Context> context,
}
Local<Array> BindingData::PackageConfig::Serialize(Realm* realm) const {
auto has_manifest = !realm->env()->options()->experimental_policy.empty();
auto isolate = realm->isolate();
const auto ToString = [isolate](std::string_view input) -> Local<Primitive> {
return String::NewFromUtf8(
isolate, input.data(), NewStringType::kNormal, input.size())
.ToLocalChecked();
};
Local<Value> values[7] = {
Local<Value> values[6] = {
name.has_value() ? ToString(*name) : Undefined(isolate),
main.has_value() ? ToString(*main) : Undefined(isolate),
ToString(type),
imports.has_value() ? ToString(*imports) : Undefined(isolate),
exports.has_value() ? ToString(*exports) : Undefined(isolate),
has_manifest ? ToString(raw_json) : Undefined(isolate),
ToString(file_path),
};
return Array::New(isolate, values, 7);
return Array::New(isolate, values, 6);
}
const BindingData::PackageConfig* BindingData::GetPackageJSON(
@ -361,11 +359,9 @@ void BindingData::GetNearestParentPackageJSONType(
return;
}
Local<Value> values[3] = {
ToV8Value(realm->context(), package_json->type).ToLocalChecked(),
ToV8Value(realm->context(), package_json->file_path).ToLocalChecked(),
ToV8Value(realm->context(), package_json->raw_json).ToLocalChecked()};
args.GetReturnValue().Set(Array::New(realm->isolate(), values, 3));
Local<Value> value =
ToV8Value(realm->context(), package_json->type).ToLocalChecked();
args.GetReturnValue().Set(value);
}
void BindingData::GetPackageScopeConfig(

View File

@ -107,14 +107,6 @@ void PerIsolateOptions::CheckOptions(std::vector<std::string>* errors,
void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
std::vector<std::string>* argv) {
if (has_policy_integrity_string && experimental_policy.empty()) {
errors->push_back("--policy-integrity requires "
"--experimental-policy be enabled");
}
if (has_policy_integrity_string && experimental_policy_integrity.empty()) {
errors->push_back("--policy-integrity cannot be empty");
}
if (!input_type.empty()) {
if (input_type != "commonjs" && input_type != "module") {
errors->push_back("--input-type must be \"module\" or \"commonjs\"");
@ -435,20 +427,6 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
&EnvironmentOptions::experimental_permission,
kAllowedInEnvvar,
false);
AddOption("--experimental-policy",
"use the specified file as a "
"security policy",
&EnvironmentOptions::experimental_policy,
kAllowedInEnvvar);
AddOption("[has_policy_integrity_string]",
"",
&EnvironmentOptions::has_policy_integrity_string);
AddOption("--policy-integrity",
"ensure the security policy contents match "
"the specified integrity",
&EnvironmentOptions::experimental_policy_integrity,
kAllowedInEnvvar);
Implies("--policy-integrity", "[has_policy_integrity_string]");
AddOption("--allow-fs-read",
"allow permissions to read the filesystem",
&EnvironmentOptions::allow_fs_read,

View File

@ -118,9 +118,6 @@ class EnvironmentOptions : public Options {
bool experimental_import_meta_resolve = false;
std::string input_type; // Value of --input-type
std::string type; // Value of --experimental-default-type
std::string experimental_policy;
std::string experimental_policy_integrity;
bool has_policy_integrity_string = false;
bool experimental_permission = false;
std::vector<std::string> allow_fs_read;
std::vector<std::string> allow_fs_write;

View File

@ -1,9 +0,0 @@
'use strict';
require('../common');
const runBenchmark = require('../common/benchmark');
runBenchmark('policy', [
'n=1',
]);

View File

@ -1,2 +0,0 @@
const os = module.constructor.createRequire('file:///os-access-module.js')('os')
os.cpus()

View File

@ -1,9 +0,0 @@
{
"resources": {
"./fhqwhgads.js": {
"dependencies": {
"**": true
}
}
}
}

View File

@ -1,2 +0,0 @@
const m = new require.main.constructor();
m.require('./invalid-module')

View File

@ -1,2 +0,0 @@
const m = new require.main.constructor();
require.extensions['.js'](m, './invalid-module')

View File

@ -1 +0,0 @@
process.mainModule.require('os').cpus();

View File

@ -1 +0,0 @@
process.mainModule.__proto__.require("os")

View File

@ -1,13 +0,0 @@
{
"resources": {
"./createRequire-bypass.js": {
"integrity": true
},
"/os-access-module.js": {
"integrity": true,
"dependencies": {
"os": true
}
}
}
}

View File

@ -1 +0,0 @@
module.constructor._load('node:child_process');

View File

@ -1,19 +0,0 @@
let requires = new WeakMap()
Object.defineProperty(Object.getPrototypeOf(module), 'require', {
get() {
return requires.get(this);
},
set(v) {
requires.set(this, v);
process.nextTick(() => {
let fs = Reflect.apply(v, this, ['fs'])
if (typeof fs.readFileSync === 'function') {
process.exit(1);
}
})
return requires.get(this);
},
configurable: true
})
require('./valid-module')

View File

@ -1,9 +0,0 @@
{
"onerror": "exit",
"scopes": {
"file:": {
"integrity": true,
"dependencies": {}
}
}
}

View File

@ -1,17 +0,0 @@
{
"onerror": "exit",
"resources": {
"./object-define-property-bypass.js": {
"integrity": true,
"dependencies": {
"./valid-module": true
}
},
"./valid-module.js": {
"integrity": true,
"dependencies": {
"fs": true
}
}
}
}

View File

@ -1 +0,0 @@
import {doesNotExist} from './dep.js';

View File

@ -1,5 +0,0 @@
import resolveAsFS from './dep.js';
import fs from 'fs';
let correct = resolveAsFS === fs && typeof resolveAsFS === 'object';
process.exit(correct ? 0 : 1);

View File

@ -1 +0,0 @@
*.js text eol=lf

View File

@ -1,3 +0,0 @@
'use strict';
// No code.

View File

@ -1,4 +0,0 @@
'use strict';
require('crypto').DEFAULT_ENCODING = process.env.DEFAULT_ENCODING;
require('./dep.js');

View File

@ -1,14 +0,0 @@
{
"resources": {
"./parent.js": {
"integrity": "sha384-j4pMdq83q5Bq9+idcHuGKzi89FrYm1PhZYrEw3irbNob6g4i3vKBjfYiRNYwmoGr",
"dependencies": {
"crypto": true,
"./dep.js": true
}
},
"./dep.js": {
"integrity": "sha384-VU7GIrTix/HFLhUb4yqsV4n1xXqjPcWw6kLvjuKXtR1+9nmufJu5vZLajBs8brIW"
}
}
}

View File

@ -1 +0,0 @@
*.js text eol=lf

View File

@ -1,8 +0,0 @@
const h = require('crypto').createHash('sha384');
const fakeDigest = h.digest();
const kHandle = Object.getOwnPropertySymbols(h)
.find((s) => s.description === 'kHandle');
h[kHandle].constructor.prototype.digest = () => fakeDigest;
require('./protected.js');

View File

@ -1,15 +0,0 @@
{
"resources": {
"./main.js": {
"integrity": true,
"dependencies": {
"./protected.js": true,
"crypto": true
}
},
"./protected.js": {
"integrity": "sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb",
"dependencies": true
}
}
}

View File

@ -1 +0,0 @@
console.log(require('fs').readFileSync('/etc/passwd').length);

View File

@ -1,7 +0,0 @@
{
"resources": {
"./dep.js": {
"integrity": "sha512-7CMcc2oytFfMnGQaXbJk84gYWF2J7p/fmWPW7dsnJyniD+vgxtK9VAZ/22UxFOA4q5d27RoGLxSqNZ/nGCJkMw== sha512-scgN9Td0bGMlGH2lUHvEeHtz92Hx6AO+sYhU3WRI6bn3jEUCXbXJs68nOOsGzRWR7a2tbqGoETnOCpHHf1Njhw=="
}
}
}

View File

@ -1,2 +0,0 @@
'use strict';
module.exports = 'The Secret Ingredient';

View File

@ -1,11 +0,0 @@
{
"resources": {
"../parent.js": {
"integrity": true,
"dependencies": {}
},
"../dep.js": {
"integrity": true
}
}
}

View File

@ -1,11 +0,0 @@
{
"resources": {
"../bad-main.mjs": {
"integrity": true,
"dependencies": true
},
"../dep.js": {
"integrity": true
}
}
}

View File

@ -1,13 +0,0 @@
{
"dependencies": true,
"resources": {
"../parent.js": {
"cascade": true,
"integrity": true
},
"../dep.js": {
"cascade": true,
"integrity": true
}
}
}

View File

@ -1,10 +0,0 @@
{
"resources": {
"../parent.js": {
"integrity": true
},
"../dep.js": {
"integrity": true
}
}
}

View File

@ -1,10 +0,0 @@
{
"resources": {
"../parent.js": {
"integrity": true,
"dependencies": {
"../dep.js": "node:util"
}
}
}
}

View File

@ -1,13 +0,0 @@
{
"resources": {
"../parent.js": {
"integrity": true,
"dependencies": {
"../dep.js": "../dep.js"
}
},
"../dep.js": {
"integrity": true
}
}
}

View File

@ -1,10 +0,0 @@
{
"resources": {
"../parent.js": {
"integrity": true,
"dependencies": {
"../dep.js": "node:404"
}
}
}
}

View File

@ -1,14 +0,0 @@
{
"resources": {
"../multi-deps.js": {
"integrity": true,
"cascade": true
}
},
"scopes": {
"../": {
"integrity": true,
"dependencies": true
}
}
}

View File

@ -1,8 +0,0 @@
{
"scopes": {
"../": {
"integrity": true,
"dependencies": true
}
}
}

View File

@ -1,12 +0,0 @@
{
"scopes": {
"file:": {
"integrity": true,
"cascade": true,
"dependencies": {
"fs": "node:fs",
"../dep.js": "node:fs"
}
}
}
}

View File

@ -1,11 +0,0 @@
{
"resources": {
"../parent.js": {
"integrity": true,
"dependencies": true
},
"../dep.js": {
"integrity": true
}
}
}

View File

@ -1,2 +0,0 @@
'use strict';
export default 'main.mjs';

View File

@ -1,3 +0,0 @@
'use strict';
require('fs');
require('process');

View File

@ -1,3 +0,0 @@
'use strict';
// Included in parent-policy.json
require('./dep.js');

View File

@ -1,10 +0,0 @@
'use strict';
const assert = require('assert');
assert.throws(() => { process.binding(); }, {
code: 'ERR_ACCESS_DENIED'
});
assert.throws(() => { process._linkedBinding(); }, {
code: 'ERR_ACCESS_DENIED'
});

View File

@ -1,10 +0,0 @@
{
"resources": {
"./app.js": {
"integrity": true,
"dependencies": {
"assert": true
}
}
}
}

View File

@ -1,17 +0,0 @@
#include <node_api.h>
#include "../../js-native-api/common.h"
#include <string.h>
static napi_value Method(napi_env env, napi_callback_info info) {
napi_value world;
const char* str = "world";
size_t str_len = strlen(str);
NODE_API_CALL(env, napi_create_string_utf8(env, str, str_len, &world));
return world;
}
NAPI_MODULE_INIT() {
napi_property_descriptor desc = DECLARE_NODE_API_PROPERTY("hello", Method);
NODE_API_CALL(env, napi_define_properties(env, exports, 1, &desc));
return exports;
}

View File

@ -1,8 +0,0 @@
{
"targets": [
{
"target_name": "binding",
"sources": [ "binding.c" ]
}
]
}

View File

@ -1,59 +0,0 @@
'use strict';
const common = require('../../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const tmpdir = require('../../common/tmpdir');
const { spawnSync } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const { pathToFileURL } = require('url');
tmpdir.refresh();
function hash(algo, body) {
const h = crypto.createHash(algo);
h.update(body);
return h.digest('base64');
}
const policyFilepath = tmpdir.resolve('policy');
const depFilepath = require.resolve(`./build/${common.buildType}/binding.node`);
const depURL = pathToFileURL(depFilepath);
const depBody = fs.readFileSync(depURL);
function writePolicy(...resources) {
const manifest = { resources: {} };
for (const { url, integrity } of resources) {
manifest.resources[url] = { integrity };
}
fs.writeFileSync(policyFilepath, JSON.stringify(manifest, null, 2));
}
function test(shouldFail, resources) {
writePolicy(...resources);
const { status, stdout, stderr } = spawnSync(process.execPath, [
'--experimental-policy',
policyFilepath,
depFilepath,
]);
console.log(stdout.toString(), stderr.toString());
if (shouldFail) {
assert.notStrictEqual(status, 0);
} else {
assert.strictEqual(status, 0);
}
}
test(false, [{
url: depURL,
integrity: `sha256-${hash('sha256', depBody)}`,
}]);
test(true, [{
url: depURL,
integrity: `sha256akjsalkjdlaskjdk-${hash('sha256', depBody)}`,
}]);

View File

@ -1,38 +0,0 @@
'use strict';
// This tests NODE_COMPILE_CACHE is disabled when policy is used.
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { spawnSyncAndAssert } = require('../common/child_process');
const assert = require('assert');
const fs = require('fs');
const tmpdir = require('../common/tmpdir');
const fixtures = require('../common/fixtures');
{
tmpdir.refresh();
const dir = tmpdir.resolve('.compile_cache_dir');
const script = fixtures.path('policy', 'parent.js');
const policy = fixtures.path(
'policy',
'dependencies',
'dependencies-redirect-policy.json');
spawnSyncAndAssert(
process.execPath,
['--experimental-policy', policy, script],
{
env: {
...process.env,
NODE_DEBUG_NATIVE: 'COMPILE_CACHE',
NODE_COMPILE_CACHE: dir
},
cwd: tmpdir.path
},
{
stderr: /skipping cache because policy is enabled/
});
assert(!fs.existsSync(dir));
}

View File

@ -1,34 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
common.requireNoPackageJSONAbove();
const fixtures = require('../common/fixtures');
const assert = require('assert');
const { spawnSync } = require('child_process');
const encodings = ['buffer', 'utf8', 'utf16le', 'latin1', 'base64', 'hex'];
for (const encoding of encodings) {
const dep = fixtures.path('policy', 'crypto-default-encoding', 'parent.js');
const depPolicy = fixtures.path(
'policy',
'crypto-default-encoding',
'policy.json');
const { status } = spawnSync(
process.execPath,
[
'--experimental-policy', depPolicy, dep,
],
{
env: {
...process.env,
DEFAULT_ENCODING: encoding
}
}
);
assert.strictEqual(status, 0);
}

View File

@ -1,21 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
common.requireNoPackageJSONAbove();
const fixtures = require('../common/fixtures');
const assert = require('assert');
const { spawnSync } = require('child_process');
const mainPath = fixtures.path('policy', 'crypto-hash-tampering', 'main.js');
const policyPath = fixtures.path(
'policy',
'crypto-hash-tampering',
'policy.json');
const { status, stderr } =
spawnSync(process.execPath, ['--experimental-policy', policyPath, mainPath], { encoding: 'utf8' });
assert.strictEqual(status, 1);
assert(stderr.includes('sha384-Bnp/T8gFNzT9mHj2G/AeuMH8LcAQ4mljw15nxBNl5yaGM7VgbMzDT7O4+dXZTJJn'));

View File

@ -1,146 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
common.requireNoPackageJSONAbove();
const fixtures = require('../common/fixtures');
const assert = require('assert');
const { spawnSync } = require('child_process');
const dep = fixtures.path('policy', 'parent.js');
{
const depPolicy = fixtures.path(
'policy',
'dependencies',
'dependencies-redirect-policy.json');
const { status, stderr, stdout } = spawnSync(
process.execPath,
[
'--experimental-policy', depPolicy, dep,
]
);
console.log('%s\n%s', stderr, stdout);
assert.strictEqual(status, 0);
}
{
const depPolicy = fixtures.path(
'policy',
'dependencies',
'dependencies-redirect-builtin-policy.json');
const { status } = spawnSync(
process.execPath,
[
'--experimental-policy', depPolicy, dep,
]
);
assert.strictEqual(status, 0);
}
{
const depPolicy = fixtures.path(
'policy',
'dependencies',
'dependencies-redirect-unknown-builtin-policy.json');
const { status } = spawnSync(
process.execPath,
[
'--experimental-policy', depPolicy, dep,
]
);
assert.strictEqual(status, 1);
}
{
const depPolicy = fixtures.path(
'policy',
'dependencies',
'dependencies-wildcard-policy.json');
const { status, stderr, stdout } = spawnSync(
process.execPath,
[
'--experimental-policy', depPolicy, dep,
]
);
console.log('%s\n%s', stderr, stdout);
assert.strictEqual(status, 0);
}
{
const depPolicy = fixtures.path(
'policy',
'dependencies',
'dependencies-empty-policy.json');
const { status } = spawnSync(
process.execPath,
[
'--experimental-policy', depPolicy, dep,
]
);
assert.strictEqual(status, 1);
}
{
const depPolicy = fixtures.path(
'policy',
'dependencies',
'dependencies-missing-policy-default-true.json');
const { status } = spawnSync(
process.execPath,
[
'--experimental-policy', depPolicy, dep,
]
);
assert.strictEqual(status, 0);
}
{
const depPolicy = fixtures.path(
'policy',
'dependencies',
'dependencies-missing-policy.json');
const { status } = spawnSync(
process.execPath,
[
'--experimental-policy', depPolicy, dep,
]
);
assert.strictEqual(status, 1);
}
{
// Regression test for https://github.com/nodejs/node/issues/37812
const depPolicy = fixtures.path(
'policy',
'dependencies',
'dependencies-missing-export-policy.json');
const { status, stderr } = spawnSync(
process.execPath,
[
'--experimental-policy',
depPolicy,
fixtures.path('policy', 'bad-main.mjs'),
]
);
assert.strictEqual(status, 1);
assert.match(
`${stderr}`,
/SyntaxError: Named export 'doesNotExist' not found\./,
new Error('Should give the real SyntaxError and position'));
}
{
const depPolicy = fixtures.path(
'policy',
'dependencies',
'dependencies-scopes-relative-specifier.json');
const { status } = spawnSync(
process.execPath,
[
'--experimental-policy',
depPolicy,
fixtures.path('policy', 'canonicalize.mjs'),
]
);
assert.strictEqual(
status,
0,
new Error(
'policies should canonicalize specifiers by default prior to matching')
);
}

View File

@ -1,123 +0,0 @@
'use strict';
// Flags: --expose-internals
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
common.requireNoPackageJSONAbove();
const Manifest = require('internal/policy/manifest').Manifest;
const assert = require('assert');
const { debuglog } = require('util');
const debug = debuglog('test');
const conditionTreePermutations = [
['default'],
['import'],
['node'],
['require'],
['require', 'import'],
['import', 'require'],
['default', 'require'],
['require', 'default'],
['node', 'require'],
['require', 'node'],
];
for (const totalDepth of [1, 2, 3]) {
const conditionTrees = [];
function calc(depthLeft = 0, path = []) {
if (depthLeft) {
for (const conditions of conditionTreePermutations) {
calc(depthLeft - 1, [...path, conditions]);
}
} else {
conditionTrees.push(path);
}
}
calc(totalDepth, []);
let nextURLId = 1;
function getUniqueHREF() {
const id = nextURLId++;
return `test:${id}`;
}
for (const tree of conditionTrees) {
const root = {};
const targets = [root];
const offsets = [-1];
const order = [];
while (offsets.length) {
const depth = offsets.length - 1;
offsets[depth]++;
const conditionOffset = offsets[depth];
const conditionsForDepth = tree[depth];
if (conditionOffset >= conditionsForDepth.length) {
offsets.pop();
continue;
}
let target;
if (depth === tree.length - 1) {
target = getUniqueHREF();
order.push({
target,
conditions: new Set(
offsets.map(
(conditionOffset, depth) => tree[depth][conditionOffset]
)
)
});
} else {
target = {};
targets[depth + 1] = target;
offsets.push(-1);
}
const condition = tree[depth][conditionOffset];
targets[depth][condition] = target;
}
const manifest = new Manifest({
resources: {
'test:_': {
dependencies: {
_: root
}
}
}
});
const redirector = manifest.getDependencyMapper('test:_');
for (const { target, conditions } of order) {
const result = redirector.resolve('_', conditions).href;
if (result !== target) {
// If we didn't hit the target, make sure a target prior to this one
// matched, including conditions
searchPriorTargets:
for (const { target: preTarget, conditions: preConditions } of order) {
if (result === preTarget) {
// Ensure that the current conditions are a super set of the
// prior target
for (const preCondition of preConditions) {
if (conditions.has(preCondition) !== true) {
continue searchPriorTargets;
}
}
break searchPriorTargets;
}
if (preTarget === target) {
debug(
'dependencies %O expected ordering %O and trying for %j ' +
'no prior targets matched',
root,
order,
target
);
// THIS WILL ALWAYS FAIL, but we want that error message
assert.strictEqual(
result, target
);
}
}
}
}
}
}

View File

@ -1,66 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
common.requireNoPackageJSONAbove();
const fixtures = require('../common/fixtures');
const assert = require('assert');
const { spawnSync } = require('child_process');
const fs = require('fs');
const crypto = require('crypto');
const depPolicy = fixtures.path('policy', 'dep-policy.json');
const dep = fixtures.path('policy', 'dep.js');
const emptyHash = crypto.createHash('sha512');
emptyHash.update('');
const emptySRI = `sha512-${emptyHash.digest('base64')}`;
const policyHash = crypto.createHash('sha512');
policyHash.update(fs.readFileSync(depPolicy));
/* eslint-disable @stylistic/js/max-len */
// When using \n only
const nixPolicySRI = 'sha512-u/nXI6UacK5fKDC2bopcgnuQY4JXJKlK3dESO3GIKKxwogVHjJqpF9rgk7Zw+TJXIc96xBUWKHuUgOzic8/4tQ==';
// When \n is turned into \r\n
const windowsPolicySRI = 'sha512-OeyCPRo4OZMosHyquZXDHpuU1F4KzG9UHFnn12FMaHsvqFUt3TFZ+7wmZE7ThZ5rsQWkUjc9ZH0knGZ2e8BYPQ==';
/* eslint-enable @stylistic/js/max-len */
const depPolicySRI = `${nixPolicySRI} ${windowsPolicySRI}`;
{
const { status, stderr } = spawnSync(
process.execPath,
[
'--policy-integrity', emptySRI,
'--experimental-policy', depPolicy, dep,
]
);
assert.ok(stderr.includes('ERR_MANIFEST_ASSERT_INTEGRITY'));
assert.strictEqual(status, 1);
}
{
const { status, stderr } = spawnSync(
process.execPath,
[
'--policy-integrity', '',
'--experimental-policy', depPolicy, dep,
]
);
assert.ok(stderr.includes('--policy-integrity'));
assert.strictEqual(status, 9);
}
{
const { status, stderr } = spawnSync(
process.execPath,
[
'--policy-integrity', depPolicySRI,
'--experimental-policy', depPolicy, dep,
]
);
assert.strictEqual(status, 0, `status: ${status}\nstderr: ${stderr}`);
}

View File

@ -1,157 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
common.requireNoPackageJSONAbove();
const assert = require('assert');
const { spawnSync } = require('child_process');
const { cpSync, rmSync } = require('fs');
const fixtures = require('../common/fixtures.js');
const tmpdir = require('../common/tmpdir.js');
{
const policyFilepath = fixtures.path('policy-manifest', 'invalid.json');
const result = spawnSync(process.execPath, [
'--experimental-policy',
policyFilepath,
'./fhqwhgads.js',
]);
assert.notStrictEqual(result.status, 0);
const stderr = result.stderr.toString();
assert.match(stderr, /ERR_MANIFEST_INVALID_SPECIFIER/);
assert.match(stderr, /pattern needs to have a single trailing "\*"/);
}
{
tmpdir.refresh();
const policyFilepath = tmpdir.resolve('file with % in its name.json');
cpSync(fixtures.path('policy-manifest', 'invalid.json'), policyFilepath);
const result = spawnSync(process.execPath, [
'--experimental-policy',
policyFilepath,
'./fhqwhgads.js',
]);
assert.notStrictEqual(result.status, 0);
const stderr = result.stderr.toString();
assert.match(stderr, /ERR_MANIFEST_INVALID_SPECIFIER/);
assert.match(stderr, /pattern needs to have a single trailing "\*"/);
rmSync(policyFilepath);
}
{
const policyFilepath = fixtures.path('policy-manifest', 'onerror-exit.json');
const result = spawnSync(process.execPath, [
'--experimental-policy',
policyFilepath,
'-e',
'require("os").cpus()',
]);
assert.notStrictEqual(result.status, 0);
const stderr = result.stderr.toString();
assert.match(stderr, /ERR_MANIFEST_DEPENDENCY_MISSING/);
assert.match(stderr, /does not list module as a dependency specifier for conditions: require, node, node-addons/);
}
{
const policyFilepath = fixtures.path('policy-manifest', 'onerror-exit.json');
const mainModuleBypass = fixtures.path('policy-manifest', 'main-module-bypass.js');
const result = spawnSync(process.execPath, [
'--experimental-policy',
policyFilepath,
mainModuleBypass,
]);
assert.notStrictEqual(result.status, 0);
const stderr = result.stderr.toString();
assert.match(stderr, /ERR_MANIFEST_DEPENDENCY_MISSING/);
assert.match(stderr, /does not list os as a dependency specifier for conditions: require, node, node-addons/);
}
{
const policyFilepath = fixtures.path('policy-manifest', 'onerror-resource-exit.json');
const objectDefinePropertyBypass = fixtures.path('policy-manifest', 'object-define-property-bypass.js');
const result = spawnSync(process.execPath, [
'--experimental-policy',
policyFilepath,
objectDefinePropertyBypass,
]);
assert.strictEqual(result.status, 0);
}
{
const policyFilepath = fixtures.path('policy-manifest', 'onerror-exit.json');
const mainModuleBypass = fixtures.path('policy-manifest', 'main-module-proto-bypass.js');
const result = spawnSync(process.execPath, [
'--experimental-policy',
policyFilepath,
mainModuleBypass,
]);
assert.notStrictEqual(result.status, 0);
const stderr = result.stderr.toString();
assert.match(stderr, /ERR_MANIFEST_DEPENDENCY_MISSING/);
assert.match(stderr, /does not list os as a dependency specifier for conditions: require, node, node-addons/);
}
{
const policyFilepath = fixtures.path('policy-manifest', 'onerror-exit.json');
const mainModuleBypass = fixtures.path('policy-manifest', 'module-constructor-bypass.js');
const result = spawnSync(process.execPath, [
'--experimental-policy',
policyFilepath,
mainModuleBypass,
]);
assert.notStrictEqual(result.status, 0);
const stderr = result.stderr.toString();
assert.match(stderr, /TypeError/);
}
{
const policyFilepath = fixtures.path('policy-manifest', 'manifest-impersonate.json');
const createRequireBypass = fixtures.path('policy-manifest', 'createRequire-bypass.js');
const result = spawnSync(process.execPath, [
'--experimental-policy',
policyFilepath,
createRequireBypass,
]);
assert.notStrictEqual(result.status, 0);
const stderr = result.stderr.toString();
assert.match(stderr, /TypeError/);
}
{
const policyFilepath = fixtures.path('policy-manifest', 'onerror-exit.json');
const mainModuleBypass = fixtures.path('policy-manifest', 'main-constructor-bypass.js');
const result = spawnSync(process.execPath, [
'--experimental-policy',
policyFilepath,
mainModuleBypass,
]);
assert.notStrictEqual(result.status, 0);
const stderr = result.stderr.toString();
assert.match(stderr, /TypeError/);
}
{
const policyFilepath = fixtures.path('policy-manifest', 'onerror-exit.json');
const mainModuleBypass = fixtures.path('policy-manifest', 'main-constructor-extensions-bypass.js');
const result = spawnSync(process.execPath, [
'--experimental-policy',
policyFilepath,
mainModuleBypass,
]);
assert.notStrictEqual(result.status, 0);
const stderr = result.stderr.toString();
assert.match(stderr, /TypeError/);
}

View File

@ -1,111 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
common.requireNoPackageJSONAbove();
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
tmpdir.refresh();
function hash(algo, body) {
const h = crypto.createHash(algo);
h.update(body);
return h.digest('base64');
}
const tmpdirPath = tmpdir.resolve('test-policy-parse-integrity');
fs.rmSync(tmpdirPath, { maxRetries: 3, recursive: true, force: true });
fs.mkdirSync(tmpdirPath, { recursive: true });
const policyFilepath = path.join(tmpdirPath, 'policy');
const parentFilepath = path.join(tmpdirPath, 'parent.js');
const parentBody = "require('./dep.js')";
const depFilepath = path.join(tmpdirPath, 'dep.js');
const depURL = pathToFileURL(depFilepath);
const depBody = '';
fs.writeFileSync(parentFilepath, parentBody);
fs.writeFileSync(depFilepath, depBody);
const tmpdirURL = pathToFileURL(tmpdirPath);
if (!tmpdirURL.pathname.endsWith('/')) {
tmpdirURL.pathname += '/';
}
const packageFilepath = path.join(tmpdirPath, 'package.json');
const packageURL = pathToFileURL(packageFilepath);
const packageBody = '{"main": "dep.js"}';
function test({ shouldFail, integrity, manifest = {} }) {
manifest.resources = {};
const resources = {
[packageURL]: {
body: packageBody,
integrity: `sha256-${hash('sha256', packageBody)}`
},
[depURL]: {
body: depBody,
integrity
}
};
for (const [url, { body, integrity }] of Object.entries(resources)) {
manifest.resources[url] = {
integrity,
};
fs.writeFileSync(new URL(url, tmpdirURL.href), body);
}
fs.writeFileSync(policyFilepath, JSON.stringify(manifest, null, 2));
const { status } = spawnSync(process.execPath, [
'--experimental-policy',
policyFilepath,
depFilepath,
]);
if (shouldFail) {
assert.notStrictEqual(status, 0);
} else {
assert.strictEqual(status, 0);
}
}
test({
shouldFail: false,
integrity: `sha256-${hash('sha256', depBody)}`,
});
test({
shouldFail: true,
integrity: `1sha256-${hash('sha256', depBody)}`,
});
test({
shouldFail: true,
integrity: 'hoge',
});
test({
shouldFail: true,
integrity: `sha256-${hash('sha256', depBody)}sha256-${hash(
'sha256',
depBody
)}`,
});
test({
shouldFail: true,
integrity: `sha256-${hash('sha256', 'file:///')}`,
manifest: {
onerror: 'exit'
}
});
test({
shouldFail: false,
integrity: `sha256-${hash('sha256', 'file:///')}`,
manifest: {
onerror: 'log'
}
});

View File

@ -1,28 +0,0 @@
'use strict';
const common = require('../common');
common.requireNoPackageJSONAbove();
if (!common.hasCrypto)
common.skip('missing crypto');
const fixtures = require('../common/fixtures');
const assert = require('node:assert');
const { spawnSync } = require('node:child_process');
const dep = fixtures.path('policy', 'process-binding', 'app.js');
const depPolicy = fixtures.path(
'policy',
'process-binding',
'policy.json');
const { status } = spawnSync(
process.execPath,
[
'--experimental-policy', depPolicy, dep,
],
{
stdio: 'inherit'
},
);
assert.strictEqual(status, 0);

View File

@ -1,342 +0,0 @@
'use strict';
// Flags: --expose-internals
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
common.requireNoPackageJSONAbove();
const Manifest = require('internal/policy/manifest').Manifest;
const assert = require('assert');
// #region files
{
const baseURLs = [
// Localhost is special cased in spec
'file://localhost/root',
'file:///root',
'file:///',
'file:///root/dir1',
'file:///root/dir1/',
'file:///root/dir1/dir2',
'file:///root/dir1/dir2/',
];
{
const manifest = new Manifest({
scopes: {
'file:///': {
dependencies: true
}
}
});
for (const href of baseURLs) {
assert.strictEqual(
manifest.getDependencyMapper(href).resolve('fs'),
true
);
}
}
{
const manifest = new Manifest({
scopes: {
'': {
dependencies: true
}
}
});
for (const href of baseURLs) {
assert.strictEqual(
manifest.getDependencyMapper(href).resolve('fs'),
true
);
}
}
{
const manifest = new Manifest({
scopes: {
'': {
dependencies: true
},
'file:': {
cascade: true
}
}
});
for (const href of baseURLs) {
assert.strictEqual(
manifest.getDependencyMapper(href).resolve('fs'),
true
);
}
}
{
const manifest = new Manifest({
scopes: {
'file:': {
dependencies: true
}
}
});
for (const href of baseURLs) {
assert.strictEqual(
manifest
.getDependencyMapper(href)
.resolve('fs'),
true);
}
assert.strictEqual(
manifest
.getDependencyMapper('file://host/')
.resolve('fs'),
true);
}
{
const manifest = new Manifest({
resources: {
'file:///root/dir1': {
dependencies: {
fs: 'test:fs1'
}
},
'file:///root/dir1/isolated': {},
'file:///root/dir1/cascade': {
cascade: true
}
},
scopes: {
'file:///root/dir1/': {
dependencies: {
fs: 'test:fs2'
}
},
'file:///root/dir1/censor/': {
},
}
});
for (const href of baseURLs) {
const redirector = manifest.getDependencyMapper(href);
if (href.startsWith('file:///root/dir1/')) {
assert.strictEqual(
redirector.resolve('fs').href,
'test:fs2'
);
} else if (href === 'file:///root/dir1') {
assert.strictEqual(
redirector.resolve('fs').href,
'test:fs1'
);
} else {
assert.strictEqual(redirector.resolve('fs'), null);
}
}
assert.strictEqual(
manifest
.getDependencyMapper('file:///root/dir1/isolated')
.resolve('fs'),
null
);
assert.strictEqual(
manifest
.getDependencyMapper('file:///root/dir1/cascade')
.resolve('fs').href,
'test:fs2'
);
assert.strictEqual(
manifest
.getDependencyMapper('file:///root/dir1/censor/foo')
.resolve('fs'),
null
);
}
}
// #endregion
// #region data
{
const baseURLs = [
'data:text/javascript,0',
'data:text/javascript,0/1',
];
{
const manifest = new Manifest({
scopes: {
'data:text/': {
dependencies: {
fs: true
}
}
}
});
for (const href of baseURLs) {
assert.strictEqual(
manifest.getDependencyMapper(href).resolve('fs'),
null);
}
}
{
const manifest = new Manifest({
scopes: {
'data:/': {
dependencies: {
fs: true
}
}
}
});
for (const href of baseURLs) {
assert.strictEqual(
manifest.getDependencyMapper(href).resolve('fs'),
null);
}
}
{
const manifest = new Manifest({
scopes: {
'data:': {
dependencies: true
}
}
});
for (const href of baseURLs) {
assert.strictEqual(
manifest.getDependencyMapper(href).resolve('fs'),
true
);
}
}
{
const manifest = new Manifest({
scopes: {
'data:text/javascript,0/': {
dependencies: {
fs: 'test:fs1'
}
},
}
});
for (const href of baseURLs) {
assert.strictEqual(
manifest.getDependencyMapper(href).resolve('fs'),
null);
}
}
}
// #endregion
// #region blob
{
{
const manifest = new Manifest({
scopes: {
'https://example.com/': {
dependencies: true
}
}
});
assert.strictEqual(
manifest
.getDependencyMapper('blob:https://example.com/has-origin')
.resolve('fs'),
true
);
}
{
const manifest = new Manifest({
scopes: {
'https://example.com': {
dependencies: true
}
}
});
assert.strictEqual(
manifest
.getDependencyMapper('blob:https://example.com/has-origin')
.resolve('fs'),
true
);
}
{
const manifest = new Manifest({
scopes: {
}
});
assert.strictEqual(
manifest
.getDependencyMapper('blob:https://example.com/has-origin')
.resolve('fs'),
null);
}
{
const manifest = new Manifest({
scopes: {
'blob:https://example.com/has-origin': {
cascade: true
}
}
});
assert.strictEqual(
manifest
.getDependencyMapper('blob:https://example.com/has-origin')
.resolve('fs'),
null);
}
{
const manifest = new Manifest({
scopes: {
// FIXME
'https://example.com/': {
dependencies: true
},
'blob:https://example.com/has-origin': {
cascade: true
}
}
});
assert.strictEqual(
manifest
.getDependencyMapper('blob:https://example.com/has-origin')
.resolve('fs'),
true
);
}
{
const manifest = new Manifest({
scopes: {
'blob:': {
dependencies: true
},
'blob:https://example.com/has-origin': {
cascade: true
}
}
});
assert.strictEqual(
manifest
.getDependencyMapper('blob:https://example.com/has-origin')
.resolve('fs'),
null);
assert.strictEqual(
manifest
.getDependencyMapper('blob:foo').resolve('fs'),
true
);
}
}
// #endregion

View File

@ -1,316 +0,0 @@
'use strict';
// Flags: --expose-internals
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
common.requireNoPackageJSONAbove();
const Manifest = require('internal/policy/manifest').Manifest;
const assert = require('assert');
// #region files
{
const baseURLs = [
// Localhost is special cased in spec
'file://localhost/root',
'file:///root',
'file:///',
'file:///root/dir1',
'file:///root/dir1/',
'file:///root/dir1/dir2',
'file:///root/dir1/dir2/',
];
{
const manifest = new Manifest({
scopes: {
'file:///': {
integrity: true
}
}
});
for (const href of baseURLs) {
assert.strictEqual(
manifest.assertIntegrity(href),
true
);
assert.strictEqual(
manifest.assertIntegrity(href, null),
true
);
assert.strictEqual(
manifest.assertIntegrity(href, ''),
true
);
}
}
{
const manifest = new Manifest({
scopes: {
'file:': {
integrity: true
}
}
});
for (const href of baseURLs) {
assert.strictEqual(
manifest.assertIntegrity(href),
true
);
assert.strictEqual(
manifest.assertIntegrity(href, null),
true
);
assert.strictEqual(
manifest.assertIntegrity(href, ''),
true
);
}
}
{
const manifest = new Manifest({
resources: {
'file:///root/dir1/isolated': {},
'file:///root/dir1/cascade': {
cascade: true
}
},
scopes: {
'file:///root/dir1/': {
integrity: true,
},
'file:///root/dir1/dir2/': {
cascade: true,
},
'file:///root/dir1/censor/': {
},
}
});
assert.throws(
() => {
manifest.assertIntegrity('file:///root/dir1/isolated');
},
/ERR_MANIFEST_ASSERT_INTEGRITY/
);
assert.strictEqual(
manifest.assertIntegrity('file:///root/dir1/cascade'),
true
);
assert.strictEqual(
manifest.assertIntegrity('file:///root/dir1/enoent'),
true
);
assert.strictEqual(
manifest.assertIntegrity('file:///root/dir1/dir2/enoent'),
true
);
assert.throws(
() => {
manifest.assertIntegrity('file:///root/dir1/censor/enoent');
},
/ERR_MANIFEST_ASSERT_INTEGRITY/
);
}
}
// #endregion
// #region data
{
const baseURLs = [
'data:text/javascript,0',
'data:text/javascript,0/1',
];
{
const manifest = new Manifest({
scopes: {
'data:text/': {
integrity: true
}
}
});
for (const href of baseURLs) {
assert.throws(
() => {
manifest.assertIntegrity(href);
},
/ERR_MANIFEST_ASSERT_INTEGRITY/
);
}
}
{
const manifest = new Manifest({
scopes: {
'data:/': {
integrity: true
}
}
});
for (const href of baseURLs) {
assert.throws(
() => {
manifest.assertIntegrity(href);
},
/ERR_MANIFEST_ASSERT_INTEGRITY/
);
}
}
{
const manifest = new Manifest({
scopes: {
'data:': {
integrity: true
}
}
});
for (const href of baseURLs) {
assert.strictEqual(manifest.assertIntegrity(href), true);
}
}
{
const manifest = new Manifest({
scopes: {
'data:text/javascript,0/': {
integrity: true
},
}
});
for (const href of baseURLs) {
assert.throws(
() => {
manifest.assertIntegrity(href);
},
/ERR_MANIFEST_ASSERT_INTEGRITY/
);
}
}
}
// #endregion
// #region blob
{
{
const manifest = new Manifest({
scopes: {
'https://example.com/': {
integrity: true
}
}
});
assert.strictEqual(
manifest.assertIntegrity('blob:https://example.com/has-origin'),
true
);
}
{
const manifest = new Manifest({
scopes: {
}
});
assert.throws(
() => {
manifest.assertIntegrity('blob:https://example.com/has-origin');
},
/ERR_MANIFEST_ASSERT_INTEGRITY/
);
}
{
const manifest = new Manifest({
scopes: {
'blob:https://example.com/has-origin': {
cascade: true
}
}
});
assert.throws(
() => {
manifest.assertIntegrity('blob:https://example.com/has-origin');
},
/ERR_MANIFEST_ASSERT_INTEGRITY/
);
}
{
const manifest = new Manifest({
resources: {
'blob:https://example.com/has-origin': {
cascade: true
}
},
scopes: {
'https://example.com': {
integrity: true
}
}
});
assert.strictEqual(
manifest.assertIntegrity('blob:https://example.com/has-origin'),
true
);
}
{
const manifest = new Manifest({
scopes: {
'blob:': {
integrity: true
},
'https://example.com': {
cascade: true
}
}
});
assert.throws(
() => {
manifest.assertIntegrity('blob:https://example.com/has-origin');
},
/ERR_MANIFEST_ASSERT_INTEGRITY/
);
assert.strictEqual(
manifest.assertIntegrity('blob:foo'),
true
);
}
}
// #endregion
// #startonerror
{
const manifest = new Manifest({
scopes: {
'file:///': {
integrity: true
}
},
onerror: 'throw'
});
assert.throws(
() => {
manifest.assertIntegrity('http://example.com');
},
/ERR_MANIFEST_ASSERT_INTEGRITY/
);
}
{
assert.throws(
() => {
new Manifest({
scopes: {
'file:///': {
integrity: true
}
},
onerror: 'unknown'
});
},
/ERR_MANIFEST_UNKNOWN_ONERROR/
);
}
// #endonerror

View File

@ -1,40 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
common.requireNoPackageJSONAbove();
const fixtures = require('../common/fixtures');
const assert = require('assert');
const { spawnSync } = require('child_process');
{
const dep = fixtures.path('policy', 'main.mjs');
const depPolicy = fixtures.path(
'policy',
'dependencies',
'dependencies-scopes-policy.json');
const { status } = spawnSync(
process.execPath,
[
'--experimental-policy', depPolicy, dep,
]
);
assert.strictEqual(status, 0);
}
{
const dep = fixtures.path('policy', 'multi-deps.js');
const depPolicy = fixtures.path(
'policy',
'dependencies',
'dependencies-scopes-and-resources-policy.json');
const { status } = spawnSync(
process.execPath,
[
'--experimental-policy', depPolicy, dep,
]
);
assert.strictEqual(status, 0);
}

View File

@ -1,365 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
if (common.isPi) {
common.skip('Too slow for Raspberry Pi devices');
}
common.requireNoPackageJSONAbove();
const { debuglog } = require('util');
const debug = debuglog('test');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync, spawn } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const cpus = require('os').availableParallelism();
function hash(algo, body) {
const values = [];
{
const h = crypto.createHash(algo);
h.update(body);
values.push(`${algo}-${h.digest('base64')}`);
}
{
const h = crypto.createHash(algo);
h.update(body.replace('\n', '\r\n'));
values.push(`${algo}-${h.digest('base64')}`);
}
return values;
}
const policyPath = './policy.json';
const parentBody = {
commonjs: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
require(process.env.DEP_FILE)
`,
module: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
import(process.env.DEP_FILE)
`,
};
let nextTestId = 1;
function newTestId() {
return nextTestId++;
}
tmpdir.refresh();
common.requireNoPackageJSONAbove(tmpdir.path);
let spawned = 0;
const toSpawn = [];
function queueSpawn(opts) {
toSpawn.push(opts);
drainQueue();
}
function drainQueue() {
if (spawned > cpus) {
return;
}
if (toSpawn.length) {
const config = toSpawn.shift();
const {
shouldSucceed,
preloads,
entryPath,
onError,
resources,
parentPath,
depPath,
} = config;
const testId = newTestId();
const configDirPath = path.join(
tmpdir.path,
`test-policy-integrity-permutation-${testId}`,
);
const tmpPolicyPath = path.join(
tmpdir.path,
`deletable-policy-${testId}.json`,
);
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
fs.mkdirSync(configDirPath, { recursive: true });
const manifest = {
onerror: onError,
resources: {},
};
const manifestPath = path.join(configDirPath, policyPath);
for (const [resourcePath, { body, integrities }] of Object.entries(
resources,
)) {
const filePath = path.join(configDirPath, resourcePath);
if (integrities !== null) {
manifest.resources[pathToFileURL(filePath).href] = {
integrity: integrities.join(' '),
dependencies: true,
};
}
fs.writeFileSync(filePath, body, 'utf8');
}
const manifestBody = JSON.stringify(manifest);
fs.writeFileSync(manifestPath, manifestBody);
if (policyPath === tmpPolicyPath) {
fs.writeFileSync(tmpPolicyPath, manifestBody);
}
const spawnArgs = [
process.execPath,
[
'--unhandled-rejections=strict',
'--experimental-policy',
policyPath,
...preloads.flatMap((m) => ['-r', m]),
entryPath,
'--',
testId,
configDirPath,
],
{
env: {
...process.env,
DELETABLE_POLICY_FILE: tmpPolicyPath,
PARENT_FILE: parentPath,
DEP_FILE: depPath,
},
cwd: configDirPath,
stdio: 'pipe',
},
];
spawned++;
const stdout = [];
const stderr = [];
const child = spawn(...spawnArgs);
child.stdout.on('data', (d) => stdout.push(d));
child.stderr.on('data', (d) => stderr.push(d));
child.on('exit', (status, signal) => {
spawned--;
try {
if (shouldSucceed) {
assert.strictEqual(status, 0);
} else {
assert.notStrictEqual(status, 0);
}
} catch (e) {
console.log(
'permutation',
testId,
'failed',
);
console.dir(
{ config, manifest },
{ depth: null },
);
console.log('exit code:', status, 'signal:', signal);
console.log(`stdout: ${Buffer.concat(stdout)}`);
console.log(`stderr: ${Buffer.concat(stderr)}`);
process.kill(process.pid, 'SIGKILL');
throw e;
}
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
drainQueue();
});
}
}
{
const { status } = spawnSync(
process.execPath,
['--experimental-policy', policyPath, '--experimental-policy', policyPath],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
}
{
const enoentFilepath = tmpdir.resolve('enoent');
try {
fs.unlinkSync(enoentFilepath);
} catch {
// Continue regardless of error.
}
const { status } = spawnSync(
process.execPath,
['--experimental-policy', enoentFilepath, '-e', ''],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow missing policies');
}
/**
* @template {Record<string, Array<string | string[] | boolean>>} T
* @param {T} configurations
* @param {object} path
* @returns {Array<{[key: keyof T]: T[keyof configurations]}>}
*/
function permutations(configurations, path = {}) {
const keys = Object.keys(configurations);
if (keys.length === 0) {
return path;
}
const config = keys[0];
const { [config]: values, ...otherConfigs } = configurations;
return values.flatMap((value) => {
return permutations(otherConfigs, { ...path, [config]: value });
});
}
const tests = new Set();
function fileExtensionFormat(extension, packageType) {
if (extension === '.js') {
return packageType === 'module' ? 'module' : 'commonjs';
} else if (extension === '.mjs') {
return 'module';
} else if (extension === '.cjs') {
return 'commonjs';
}
throw new Error('unknown format ' + extension);
}
for (const permutation of permutations({
preloads: [[], ['parent'], ['dep']],
onError: ['log', 'exit'],
parentExtension: ['.js', '.mjs', '.cjs'],
parentIntegrity: ['match', 'invalid', 'missing'],
depExtension: ['.js', '.mjs', '.cjs'],
depIntegrity: ['match', 'invalid', 'missing'],
packageType: ['no-package-json', 'module', 'commonjs'],
packageIntegrity: ['match', 'invalid', 'missing'],
})) {
let shouldSucceed = true;
const parentPath = `./parent${permutation.parentExtension}`;
const effectivePackageType =
permutation.packageType === 'module' ? 'module' : 'commonjs';
const parentFormat = fileExtensionFormat(
permutation.parentExtension,
effectivePackageType,
);
const depFormat = fileExtensionFormat(
permutation.depExtension,
effectivePackageType,
);
// non-sensical attempt to require ESM
if (depFormat === 'module' && parentFormat === 'commonjs') {
continue;
}
const depPath = `./dep${permutation.depExtension}`;
const packageJSON = {
main: depPath,
type: permutation.packageType,
};
if (permutation.packageType === 'no-field') {
delete packageJSON.type;
}
const resources = {
[depPath]: {
body: '',
integrities: hash('sha256', ''),
},
};
if (permutation.depIntegrity === 'invalid') {
resources[depPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.depIntegrity === 'missing') {
resources[depPath].integrities = null;
shouldSucceed = false;
} else if (permutation.depIntegrity !== 'match') {
throw new Error('unreachable');
}
if (parentFormat !== 'commonjs') {
permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent');
}
const hasParent = permutation.preloads.includes('parent');
if (hasParent) {
resources[parentPath] = {
body: parentBody[parentFormat],
integrities: hash('sha256', parentBody[parentFormat]),
};
if (permutation.parentIntegrity === 'invalid') {
resources[parentPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.parentIntegrity === 'missing') {
resources[parentPath].integrities = null;
shouldSucceed = false;
} else if (permutation.parentIntegrity !== 'match') {
throw new Error('unreachable');
}
}
if (permutation.packageType !== 'no-package-json') {
let packageBody = JSON.stringify(packageJSON, null, 2);
let packageIntegrities = hash('sha256', packageBody);
if (
permutation.parentExtension !== '.js' ||
permutation.depExtension !== '.js'
) {
// NO PACKAGE LOOKUP
continue;
}
if (permutation.packageIntegrity === 'invalid') {
packageJSON['//'] = 'INVALID INTEGRITY';
packageBody = JSON.stringify(packageJSON, null, 2);
shouldSucceed = false;
} else if (permutation.packageIntegrity === 'missing') {
packageIntegrities = [];
shouldSucceed = false;
} else if (permutation.packageIntegrity !== 'match') {
throw new Error('unreachable');
}
resources['./package.json'] = {
body: packageBody,
integrities: packageIntegrities,
};
}
if (permutation.onError === 'log') {
shouldSucceed = true;
}
tests.add(
JSON.stringify({
onError: permutation.onError,
shouldSucceed,
entryPath: depPath,
preloads: permutation.preloads
.map((_) => {
return {
'': '',
'parent': parentFormat === 'commonjs' ? parentPath : '',
'dep': depFormat === 'commonjs' ? depPath : '',
}[_];
})
.filter(Boolean),
parentPath,
depPath,
resources,
}),
);
}
debug(`spawning ${tests.size} policy integrity permutations`);
for (const config of tests) {
const parsed = JSON.parse(config);
queueSpawn(parsed);
}

View File

@ -1,352 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
if (common.isPi) {
common.skip('Too slow for Raspberry Pi devices');
}
common.requireNoPackageJSONAbove();
const { debuglog } = require('util');
const debug = debuglog('test');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync, spawn } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const cpus = require('os').availableParallelism();
function hash(algo, body) {
const values = [];
{
const h = crypto.createHash(algo);
h.update(body);
values.push(`${algo}-${h.digest('base64')}`);
}
{
const h = crypto.createHash(algo);
h.update(body.replace('\n', '\r\n'));
values.push(`${algo}-${h.digest('base64')}`);
}
return values;
}
const policyPath = './policy.json';
const parentBody = {
commonjs: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
require(process.env.DEP_FILE)
`,
module: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
import(process.env.DEP_FILE)
`,
};
let nextTestId = 1;
function newTestId() {
return nextTestId++;
}
tmpdir.refresh();
common.requireNoPackageJSONAbove(tmpdir.path);
let spawned = 0;
const toSpawn = [];
function queueSpawn(opts) {
toSpawn.push(opts);
drainQueue();
}
function drainQueue() {
if (spawned > cpus) {
return;
}
if (toSpawn.length) {
const config = toSpawn.shift();
const {
shouldSucceed,
preloads,
entryPath,
willDeletePolicy,
onError,
resources,
parentPath,
depPath,
} = config;
const testId = newTestId();
const configDirPath = path.join(
tmpdir.path,
`test-policy-integrity-permutation-${testId}`,
);
const tmpPolicyPath = path.join(
tmpdir.path,
`deletable-policy-${testId}.json`,
);
const cliPolicy = willDeletePolicy ? tmpPolicyPath : policyPath;
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
fs.mkdirSync(configDirPath, { recursive: true });
const manifest = {
onerror: onError,
resources: {},
};
const manifestPath = path.join(configDirPath, policyPath);
for (const [resourcePath, { body, integrities }] of Object.entries(
resources,
)) {
const filePath = path.join(configDirPath, resourcePath);
if (integrities !== null) {
manifest.resources[pathToFileURL(filePath).href] = {
integrity: integrities.join(' '),
dependencies: true,
};
}
fs.writeFileSync(filePath, body, 'utf8');
}
const manifestBody = JSON.stringify(manifest);
fs.writeFileSync(manifestPath, manifestBody);
if (cliPolicy === tmpPolicyPath) {
fs.writeFileSync(tmpPolicyPath, manifestBody);
}
const spawnArgs = [
process.execPath,
[
'--unhandled-rejections=strict',
'--experimental-policy',
cliPolicy,
...preloads.flatMap((m) => ['-r', m]),
entryPath,
'--',
testId,
configDirPath,
],
{
env: {
...process.env,
DELETABLE_POLICY_FILE: tmpPolicyPath,
PARENT_FILE: parentPath,
DEP_FILE: depPath,
},
cwd: configDirPath,
stdio: 'pipe',
},
];
spawned++;
const stdout = [];
const stderr = [];
const child = spawn(...spawnArgs);
child.stdout.on('data', (d) => stdout.push(d));
child.stderr.on('data', (d) => stderr.push(d));
child.on('exit', (status, signal) => {
spawned--;
try {
if (shouldSucceed) {
assert.strictEqual(status, 0);
} else {
assert.notStrictEqual(status, 0);
}
} catch (e) {
console.log(
'permutation',
testId,
'failed',
);
console.dir(
{ config, manifest },
{ depth: null },
);
console.log('exit code:', status, 'signal:', signal);
console.log(`stdout: ${Buffer.concat(stdout)}`);
console.log(`stderr: ${Buffer.concat(stderr)}`);
throw e;
}
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
drainQueue();
});
}
}
{
const { status } = spawnSync(
process.execPath,
['--experimental-policy', policyPath, '--experimental-policy', policyPath],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
}
{
const enoentFilepath = tmpdir.resolve('enoent');
try {
fs.unlinkSync(enoentFilepath);
} catch {
// Continue regardless of error.
}
const { status } = spawnSync(
process.execPath,
['--experimental-policy', enoentFilepath, '-e', ''],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow missing policies');
}
/**
* @template {Record<string, Array<string | string[] | boolean>>} T
* @param {T} configurations
* @param {object} path
* @returns {Array<{[key: keyof T]: T[keyof configurations]}>}
*/
function permutations(configurations, path = {}) {
const keys = Object.keys(configurations);
if (keys.length === 0) {
return path;
}
const config = keys[0];
const { [config]: values, ...otherConfigs } = configurations;
return values.flatMap((value) => {
return permutations(otherConfigs, { ...path, [config]: value });
});
}
const tests = new Set();
function fileExtensionFormat(extension) {
if (extension === '.js') {
return 'commonjs';
} else if (extension === '.mjs') {
return 'module';
} else if (extension === '.cjs') {
return 'commonjs';
}
throw new Error('unknown format ' + extension);
}
for (const permutation of permutations({
preloads: [[], ['parent'], ['dep']],
onError: ['log', 'exit'],
parentExtension: ['.js', '.mjs', '.cjs'],
parentIntegrity: ['match', 'invalid', 'missing'],
depExtension: ['.js', '.mjs', '.cjs'],
depIntegrity: ['match', 'invalid', 'missing'],
packageIntegrity: ['match', 'invalid', 'missing'],
})) {
let shouldSucceed = true;
const parentPath = `./parent${permutation.parentExtension}`;
const parentFormat = fileExtensionFormat(permutation.parentExtension);
const depFormat = fileExtensionFormat(permutation.depExtension);
// non-sensical attempt to require ESM
if (depFormat === 'module' && parentFormat === 'commonjs') {
continue;
}
const depPath = `./dep${permutation.depExtension}`;
const entryPath = parentPath;
const packageJSON = {
main: entryPath,
type: 'commonjs',
};
const resources = {
[depPath]: {
body: '',
integrities: hash('sha256', ''),
},
};
if (permutation.depIntegrity === 'invalid') {
resources[depPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.depIntegrity === 'missing') {
resources[depPath].integrities = null;
shouldSucceed = false;
} else if (permutation.depIntegrity !== 'match') {
throw new Error('unreachable');
}
if (parentFormat !== 'commonjs') {
permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent');
}
resources[parentPath] = {
body: parentBody[parentFormat],
integrities: hash('sha256', parentBody[parentFormat]),
};
if (permutation.parentIntegrity === 'invalid') {
resources[parentPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.parentIntegrity === 'missing') {
resources[parentPath].integrities = null;
shouldSucceed = false;
} else if (permutation.parentIntegrity !== 'match') {
throw new Error('unreachable');
}
let packageBody = JSON.stringify(packageJSON, null, 2);
let packageIntegrities = hash('sha256', packageBody);
if (
permutation.parentExtension !== '.js' ||
permutation.depExtension !== '.js'
) {
// NO PACKAGE LOOKUP
continue;
}
if (permutation.packageIntegrity === 'invalid') {
packageJSON['//'] = 'INVALID INTEGRITY';
packageBody = JSON.stringify(packageJSON, null, 2);
shouldSucceed = false;
} else if (permutation.packageIntegrity === 'missing') {
packageIntegrities = [];
shouldSucceed = false;
} else if (permutation.packageIntegrity !== 'match') {
throw new Error('unreachable');
}
resources['./package.json'] = {
body: packageBody,
integrities: packageIntegrities,
};
if (permutation.onError === 'log') {
shouldSucceed = true;
}
tests.add(
JSON.stringify({
onError: permutation.onError,
shouldSucceed,
entryPath,
willDeletePolicy: false,
preloads: permutation.preloads
.map((_) => {
return {
'': '',
'parent': parentFormat === 'commonjs' ? parentPath : '',
'dep': depFormat === 'commonjs' ? depPath : '',
}[_];
})
.filter(Boolean),
parentPath,
depPath,
resources,
}),
);
}
debug(`spawning ${tests.size} policy integrity permutations`);
for (const config of tests) {
const parsed = JSON.parse(config);
queueSpawn(parsed);
}

View File

@ -1,352 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
if (common.isPi) {
common.skip('Too slow for Raspberry Pi devices');
}
common.requireNoPackageJSONAbove();
const { debuglog } = require('util');
const debug = debuglog('test');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync, spawn } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const cpus = require('os').availableParallelism();
function hash(algo, body) {
const values = [];
{
const h = crypto.createHash(algo);
h.update(body);
values.push(`${algo}-${h.digest('base64')}`);
}
{
const h = crypto.createHash(algo);
h.update(body.replace('\n', '\r\n'));
values.push(`${algo}-${h.digest('base64')}`);
}
return values;
}
const policyPath = './policy.json';
const parentBody = {
commonjs: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
require(process.env.DEP_FILE)
`,
module: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
import(process.env.DEP_FILE)
`,
};
let nextTestId = 1;
function newTestId() {
return nextTestId++;
}
tmpdir.refresh();
common.requireNoPackageJSONAbove(tmpdir.path);
let spawned = 0;
const toSpawn = [];
function queueSpawn(opts) {
toSpawn.push(opts);
drainQueue();
}
function drainQueue() {
if (spawned > cpus) {
return;
}
if (toSpawn.length) {
const config = toSpawn.shift();
const {
shouldSucceed,
preloads,
entryPath,
willDeletePolicy,
onError,
resources,
parentPath,
depPath,
} = config;
const testId = newTestId();
const configDirPath = path.join(
tmpdir.path,
`test-policy-integrity-permutation-${testId}`,
);
const tmpPolicyPath = path.join(
tmpdir.path,
`deletable-policy-${testId}.json`,
);
const cliPolicy = willDeletePolicy ? tmpPolicyPath : policyPath;
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
fs.mkdirSync(configDirPath, { recursive: true });
const manifest = {
onerror: onError,
resources: {},
};
const manifestPath = path.join(configDirPath, policyPath);
for (const [resourcePath, { body, integrities }] of Object.entries(
resources,
)) {
const filePath = path.join(configDirPath, resourcePath);
if (integrities !== null) {
manifest.resources[pathToFileURL(filePath).href] = {
integrity: integrities.join(' '),
dependencies: true,
};
}
fs.writeFileSync(filePath, body, 'utf8');
}
const manifestBody = JSON.stringify(manifest);
fs.writeFileSync(manifestPath, manifestBody);
if (cliPolicy === tmpPolicyPath) {
fs.writeFileSync(tmpPolicyPath, manifestBody);
}
const spawnArgs = [
process.execPath,
[
'--unhandled-rejections=strict',
'--experimental-policy',
cliPolicy,
...preloads.flatMap((m) => ['-r', m]),
entryPath,
'--',
testId,
configDirPath,
],
{
env: {
...process.env,
DELETABLE_POLICY_FILE: tmpPolicyPath,
PARENT_FILE: parentPath,
DEP_FILE: depPath,
},
cwd: configDirPath,
stdio: 'pipe',
},
];
spawned++;
const stdout = [];
const stderr = [];
const child = spawn(...spawnArgs);
child.stdout.on('data', (d) => stdout.push(d));
child.stderr.on('data', (d) => stderr.push(d));
child.on('exit', (status, signal) => {
spawned--;
try {
if (shouldSucceed) {
assert.strictEqual(status, 0);
} else {
assert.notStrictEqual(status, 0);
}
} catch (e) {
console.log(
'permutation',
testId,
'failed',
);
console.dir(
{ config, manifest },
{ depth: null },
);
console.log('exit code:', status, 'signal:', signal);
console.log(`stdout: ${Buffer.concat(stdout)}`);
console.log(`stderr: ${Buffer.concat(stderr)}`);
throw e;
}
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
drainQueue();
});
}
}
{
const { status } = spawnSync(
process.execPath,
['--experimental-policy', policyPath, '--experimental-policy', policyPath],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
}
{
const enoentFilepath = tmpdir.resolve('enoent');
try {
fs.unlinkSync(enoentFilepath);
} catch {
// Continue regardless of error.
}
const { status } = spawnSync(
process.execPath,
['--experimental-policy', enoentFilepath, '-e', ''],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow missing policies');
}
/**
* @template {Record<string, Array<string | string[] | boolean>>} T
* @param {T} configurations
* @param {object} path
* @returns {Array<{[key: keyof T]: T[keyof configurations]}>}
*/
function permutations(configurations, path = {}) {
const keys = Object.keys(configurations);
if (keys.length === 0) {
return path;
}
const config = keys[0];
const { [config]: values, ...otherConfigs } = configurations;
return values.flatMap((value) => {
return permutations(otherConfigs, { ...path, [config]: value });
});
}
const tests = new Set();
function fileExtensionFormat(extension) {
if (extension === '.js') {
return 'module';
} else if (extension === '.mjs') {
return 'module';
} else if (extension === '.cjs') {
return 'commonjs';
}
throw new Error('unknown format ' + extension);
}
for (const permutation of permutations({
preloads: [[], ['parent'], ['dep']],
onError: ['log', 'exit'],
parentExtension: ['.js', '.mjs', '.cjs'],
parentIntegrity: ['match', 'invalid', 'missing'],
depExtension: ['.js', '.mjs', '.cjs'],
depIntegrity: ['match', 'invalid', 'missing'],
packageIntegrity: ['match', 'invalid', 'missing'],
})) {
let shouldSucceed = true;
const parentPath = `./parent${permutation.parentExtension}`;
const parentFormat = fileExtensionFormat(permutation.parentExtension);
const depFormat = fileExtensionFormat(permutation.depExtension);
// non-sensical attempt to require ESM
if (depFormat === 'module' && parentFormat === 'commonjs') {
continue;
}
const depPath = `./dep${permutation.depExtension}`;
const entryPath = parentPath;
const packageJSON = {
main: entryPath,
type: 'module',
};
const resources = {
[depPath]: {
body: '',
integrities: hash('sha256', ''),
},
};
if (permutation.depIntegrity === 'invalid') {
resources[depPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.depIntegrity === 'missing') {
resources[depPath].integrities = null;
shouldSucceed = false;
} else if (permutation.depIntegrity !== 'match') {
throw new Error('unreachable');
}
if (parentFormat !== 'commonjs') {
permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent');
}
resources[parentPath] = {
body: parentBody[parentFormat],
integrities: hash('sha256', parentBody[parentFormat]),
};
if (permutation.parentIntegrity === 'invalid') {
resources[parentPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.parentIntegrity === 'missing') {
resources[parentPath].integrities = null;
shouldSucceed = false;
} else if (permutation.parentIntegrity !== 'match') {
throw new Error('unreachable');
}
let packageBody = JSON.stringify(packageJSON, null, 2);
let packageIntegrities = hash('sha256', packageBody);
if (
permutation.parentExtension !== '.js' ||
permutation.depExtension !== '.js'
) {
// NO PACKAGE LOOKUP
continue;
}
if (permutation.packageIntegrity === 'invalid') {
packageJSON['//'] = 'INVALID INTEGRITY';
packageBody = JSON.stringify(packageJSON, null, 2);
shouldSucceed = false;
} else if (permutation.packageIntegrity === 'missing') {
packageIntegrities = [];
shouldSucceed = false;
} else if (permutation.packageIntegrity !== 'match') {
throw new Error('unreachable');
}
resources['./package.json'] = {
body: packageBody,
integrities: packageIntegrities,
};
if (permutation.onError === 'log') {
shouldSucceed = true;
}
tests.add(
JSON.stringify({
onError: permutation.onError,
shouldSucceed,
entryPath,
willDeletePolicy: false,
preloads: permutation.preloads
.map((_) => {
return {
'': '',
'parent': parentFormat === 'commonjs' ? parentPath : '',
'dep': depFormat === 'commonjs' ? depPath : '',
}[_];
})
.filter(Boolean),
parentPath,
depPath,
resources,
}),
);
}
debug(`spawning ${tests.size} policy integrity permutations`);
for (const config of tests) {
const parsed = JSON.parse(config);
queueSpawn(parsed);
}

View File

@ -1,324 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
if (common.isPi) {
common.skip('Too slow for Raspberry Pi devices');
}
common.requireNoPackageJSONAbove();
const { debuglog } = require('util');
const debug = debuglog('test');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync, spawn } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const cpus = require('os').availableParallelism();
function hash(algo, body) {
const values = [];
{
const h = crypto.createHash(algo);
h.update(body);
values.push(`${algo}-${h.digest('base64')}`);
}
{
const h = crypto.createHash(algo);
h.update(body.replace('\n', '\r\n'));
values.push(`${algo}-${h.digest('base64')}`);
}
return values;
}
const policyPath = './policy.json';
const parentBody = {
commonjs: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
require(process.env.DEP_FILE)
`,
module: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
import(process.env.DEP_FILE)
`,
};
let nextTestId = 1;
function newTestId() {
return nextTestId++;
}
tmpdir.refresh();
common.requireNoPackageJSONAbove(tmpdir.path);
let spawned = 0;
const toSpawn = [];
function queueSpawn(opts) {
toSpawn.push(opts);
drainQueue();
}
function drainQueue() {
if (spawned > cpus) {
return;
}
if (toSpawn.length) {
const config = toSpawn.shift();
const {
shouldSucceed,
preloads,
entryPath,
willDeletePolicy,
onError,
resources,
parentPath,
depPath,
} = config;
const testId = newTestId();
const configDirPath = path.join(
tmpdir.path,
`test-policy-integrity-permutation-${testId}`,
);
const tmpPolicyPath = path.join(
tmpdir.path,
`deletable-policy-${testId}.json`,
);
const cliPolicy = willDeletePolicy ? tmpPolicyPath : policyPath;
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
fs.mkdirSync(configDirPath, { recursive: true });
const manifest = {
onerror: onError,
resources: {},
};
const manifestPath = path.join(configDirPath, policyPath);
for (const [resourcePath, { body, integrities }] of Object.entries(
resources,
)) {
const filePath = path.join(configDirPath, resourcePath);
if (integrities !== null) {
manifest.resources[pathToFileURL(filePath).href] = {
integrity: integrities.join(' '),
dependencies: true,
};
}
fs.writeFileSync(filePath, body, 'utf8');
}
const manifestBody = JSON.stringify(manifest);
fs.writeFileSync(manifestPath, manifestBody);
if (cliPolicy === tmpPolicyPath) {
fs.writeFileSync(tmpPolicyPath, manifestBody);
}
const spawnArgs = [
process.execPath,
[
'--unhandled-rejections=strict',
'--experimental-policy',
cliPolicy,
...preloads.flatMap((m) => ['-r', m]),
entryPath,
'--',
testId,
configDirPath,
],
{
env: {
...process.env,
DELETABLE_POLICY_FILE: tmpPolicyPath,
PARENT_FILE: parentPath,
DEP_FILE: depPath,
},
cwd: configDirPath,
stdio: 'pipe',
},
];
spawned++;
const stdout = [];
const stderr = [];
const child = spawn(...spawnArgs);
child.stdout.on('data', (d) => stdout.push(d));
child.stderr.on('data', (d) => stderr.push(d));
child.on('exit', (status, signal) => {
spawned--;
try {
if (shouldSucceed) {
assert.strictEqual(status, 0);
} else {
assert.notStrictEqual(status, 0);
}
} catch (e) {
console.log(
'permutation',
testId,
'failed',
);
console.dir(
{ config, manifest },
{ depth: null },
);
console.log('exit code:', status, 'signal:', signal);
console.log(`stdout: ${Buffer.concat(stdout)}`);
console.log(`stderr: ${Buffer.concat(stderr)}`);
throw e;
}
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
drainQueue();
});
}
}
{
const { status } = spawnSync(
process.execPath,
['--experimental-policy', policyPath, '--experimental-policy', policyPath],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
}
{
const enoentFilepath = tmpdir.resolve('enoent');
try {
fs.unlinkSync(enoentFilepath);
} catch {
// Continue regardless of error.
}
const { status } = spawnSync(
process.execPath,
['--experimental-policy', enoentFilepath, '-e', ''],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow missing policies');
}
/**
* @template {Record<string, Array<string | string[] | boolean>>} T
* @param {T} configurations
* @param {object} path
* @returns {Array<{[key: keyof T]: T[keyof configurations]}>}
*/
function permutations(configurations, path = {}) {
const keys = Object.keys(configurations);
if (keys.length === 0) {
return path;
}
const config = keys[0];
const { [config]: values, ...otherConfigs } = configurations;
return values.flatMap((value) => {
return permutations(otherConfigs, { ...path, [config]: value });
});
}
const tests = new Set();
function fileExtensionFormat(extension) {
if (extension === '.js') {
return 'commonjs';
} else if (extension === '.mjs') {
return 'module';
} else if (extension === '.cjs') {
return 'commonjs';
}
throw new Error('unknown format ' + extension);
}
for (const permutation of permutations({
preloads: [[], ['parent'], ['dep']],
onError: ['log', 'exit'],
parentExtension: ['.js', '.mjs', '.cjs'],
parentIntegrity: ['match', 'invalid', 'missing'],
depExtension: ['.js', '.mjs', '.cjs'],
depIntegrity: ['match', 'invalid', 'missing'],
packageIntegrity: ['match', 'invalid', 'missing'],
})) {
let shouldSucceed = true;
const parentPath = `./parent${permutation.parentExtension}`;
const parentFormat = fileExtensionFormat(permutation.parentExtension);
const depFormat = fileExtensionFormat(permutation.depExtension);
// non-sensical attempt to require ESM
if (depFormat === 'module' && parentFormat === 'commonjs') {
continue;
}
const depPath = `./dep${permutation.depExtension}`;
const entryPath = parentPath;
const resources = {
[depPath]: {
body: '',
integrities: hash('sha256', ''),
},
};
if (permutation.depIntegrity === 'invalid') {
resources[depPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.depIntegrity === 'missing') {
resources[depPath].integrities = null;
shouldSucceed = false;
} else if (permutation.depIntegrity !== 'match') {
throw new Error('unreachable');
}
if (parentFormat !== 'commonjs') {
permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent');
}
resources[parentPath] = {
body: parentBody[parentFormat],
integrities: hash('sha256', parentBody[parentFormat]),
};
if (permutation.parentIntegrity === 'invalid') {
resources[parentPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.parentIntegrity === 'missing') {
resources[parentPath].integrities = null;
shouldSucceed = false;
} else if (permutation.parentIntegrity !== 'match') {
throw new Error('unreachable');
}
if (permutation.onError === 'log') {
shouldSucceed = true;
}
tests.add(
JSON.stringify({
onError: permutation.onError,
shouldSucceed,
entryPath,
willDeletePolicy: false,
preloads: permutation.preloads
.map((_) => {
return {
'': '',
'parent': parentFormat === 'commonjs' ? parentPath : '',
'dep': depFormat === 'commonjs' ? depPath : '',
}[_];
})
.filter(Boolean),
parentPath,
depPath,
resources,
}),
);
}
debug(`spawning ${tests.size} policy integrity permutations`);
for (const config of tests) {
const parsed = JSON.parse(config);
queueSpawn(parsed);
}

View File

@ -1,375 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
if (common.isPi) {
common.skip('Too slow for Raspberry Pi devices');
}
common.requireNoPackageJSONAbove();
const { debuglog } = require('util');
const debug = debuglog('test');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync, spawn } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const cpus = require('os').availableParallelism();
function hash(algo, body) {
const values = [];
{
const h = crypto.createHash(algo);
h.update(body);
values.push(`${algo}-${h.digest('base64')}`);
}
{
const h = crypto.createHash(algo);
h.update(body.replace('\n', '\r\n'));
values.push(`${algo}-${h.digest('base64')}`);
}
return values;
}
const policyPath = './policy.json';
const parentBody = {
commonjs: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
require(process.env.DEP_FILE)
`,
module: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
import(process.env.DEP_FILE)
`,
};
const workerSpawningBody = `
const path = require('path');
const { Worker } = require('worker_threads');
if (!process.env.PARENT_FILE) {
console.error(
'missing required PARENT_FILE env to determine worker entry point'
);
process.exit(33);
}
if (!process.env.DELETABLE_POLICY_FILE) {
console.error(
'missing required DELETABLE_POLICY_FILE env to check reloading'
);
process.exit(33);
}
const w = new Worker(path.resolve(process.env.PARENT_FILE));
w.on('exit', (status) => process.exit(status === 0 ? 0 : 1));
`;
let nextTestId = 1;
function newTestId() {
return nextTestId++;
}
tmpdir.refresh();
common.requireNoPackageJSONAbove(tmpdir.path);
let spawned = 0;
const toSpawn = [];
function queueSpawn(opts) {
toSpawn.push(opts);
drainQueue();
}
function drainQueue() {
if (spawned > cpus) {
return;
}
if (toSpawn.length) {
const config = toSpawn.shift();
const {
shouldSucceed,
preloads,
entryPath,
onError,
resources,
parentPath,
depPath,
} = config;
const testId = newTestId();
const configDirPath = path.join(
tmpdir.path,
`test-policy-integrity-permutation-${testId}`,
);
const tmpPolicyPath = path.join(
tmpdir.path,
`deletable-policy-${testId}.json`,
);
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
fs.mkdirSync(configDirPath, { recursive: true });
const manifest = {
onerror: onError,
resources: {},
};
const manifestPath = path.join(configDirPath, policyPath);
for (const [resourcePath, { body, integrities }] of Object.entries(
resources,
)) {
const filePath = path.join(configDirPath, resourcePath);
if (integrities !== null) {
manifest.resources[pathToFileURL(filePath).href] = {
integrity: integrities.join(' '),
dependencies: true,
};
}
fs.writeFileSync(filePath, body, 'utf8');
}
const manifestBody = JSON.stringify(manifest);
fs.writeFileSync(manifestPath, manifestBody);
fs.writeFileSync(tmpPolicyPath, manifestBody);
const spawnArgs = [
process.execPath,
[
'--unhandled-rejections=strict',
'--experimental-policy',
tmpPolicyPath,
...preloads.flatMap((m) => ['-r', m]),
entryPath,
'--',
testId,
configDirPath,
],
{
env: {
...process.env,
DELETABLE_POLICY_FILE: tmpPolicyPath,
PARENT_FILE: parentPath,
DEP_FILE: depPath,
},
cwd: configDirPath,
stdio: 'pipe',
},
];
spawned++;
const stdout = [];
const stderr = [];
const child = spawn(...spawnArgs);
child.stdout.on('data', (d) => stdout.push(d));
child.stderr.on('data', (d) => stderr.push(d));
child.on('exit', (status, signal) => {
spawned--;
try {
if (shouldSucceed) {
assert.strictEqual(status, 0);
} else {
assert.notStrictEqual(status, 0);
}
} catch (e) {
console.log(
'permutation',
testId,
'failed',
);
console.dir(
{ config, manifest },
{ depth: null },
);
console.log('exit code:', status, 'signal:', signal);
console.log(`stdout: ${Buffer.concat(stdout)}`);
console.log(`stderr: ${Buffer.concat(stderr)}`);
throw e;
}
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
drainQueue();
});
}
}
{
const { status } = spawnSync(
process.execPath,
['--experimental-policy', policyPath, '--experimental-policy', policyPath],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
}
{
const enoentFilepath = tmpdir.resolve('enoent');
try {
fs.unlinkSync(enoentFilepath);
} catch {
// Continue regardless of error.
}
const { status } = spawnSync(
process.execPath,
['--experimental-policy', enoentFilepath, '-e', ''],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow missing policies');
}
/**
* @template {Record<string, Array<string | string[] | boolean>>} T
* @param {T} configurations
* @param {object} path
* @returns {Array<{[key: keyof T]: T[keyof configurations]}>}
*/
function permutations(configurations, path = {}) {
const keys = Object.keys(configurations);
if (keys.length === 0) {
return path;
}
const config = keys[0];
const { [config]: values, ...otherConfigs } = configurations;
return values.flatMap((value) => {
return permutations(otherConfigs, { ...path, [config]: value });
});
}
const tests = new Set();
function fileExtensionFormat(extension) {
if (extension === '.js') {
return 'commonjs';
} else if (extension === '.mjs') {
return 'module';
} else if (extension === '.cjs') {
return 'commonjs';
}
throw new Error('unknown format ' + extension);
}
for (const permutation of permutations({
preloads: [[], ['parent'], ['dep']],
onError: ['log', 'exit'],
parentExtension: ['.js', '.mjs', '.cjs'],
parentIntegrity: ['match', 'invalid', 'missing'],
depExtension: ['.js', '.mjs', '.cjs'],
depIntegrity: ['match', 'invalid', 'missing'],
packageIntegrity: ['match', 'invalid', 'missing'],
})) {
let shouldSucceed = true;
const parentPath = `./parent${permutation.parentExtension}`;
const parentFormat = fileExtensionFormat(permutation.parentExtension);
const depFormat = fileExtensionFormat(permutation.depExtension);
// non-sensical attempt to require ESM
if (depFormat === 'module' && parentFormat === 'commonjs') {
continue;
}
const depPath = `./dep${permutation.depExtension}`;
const workerSpawnerPath = './worker-spawner.cjs';
const packageJSON = {
main: workerSpawnerPath,
type: 'commonjs',
};
const resources = {
[depPath]: {
body: '',
integrities: hash('sha256', ''),
},
};
if (permutation.depIntegrity === 'invalid') {
resources[depPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.depIntegrity === 'missing') {
resources[depPath].integrities = null;
shouldSucceed = false;
} else if (permutation.depIntegrity !== 'match') {
throw new Error('unreachable');
}
if (parentFormat !== 'commonjs') {
permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent');
}
resources[parentPath] = {
body: parentBody[parentFormat],
integrities: hash('sha256', parentBody[parentFormat]),
};
if (permutation.parentIntegrity === 'invalid') {
resources[parentPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.parentIntegrity === 'missing') {
resources[parentPath].integrities = null;
shouldSucceed = false;
} else if (permutation.parentIntegrity !== 'match') {
throw new Error('unreachable');
}
resources[workerSpawnerPath] = {
body: workerSpawningBody,
integrities: hash('sha256', workerSpawningBody),
};
let packageBody = JSON.stringify(packageJSON, null, 2);
let packageIntegrities = hash('sha256', packageBody);
if (
permutation.parentExtension !== '.js' ||
permutation.depExtension !== '.js'
) {
// NO PACKAGE LOOKUP
continue;
}
if (permutation.packageIntegrity === 'invalid') {
packageJSON['//'] = 'INVALID INTEGRITY';
packageBody = JSON.stringify(packageJSON, null, 2);
shouldSucceed = false;
} else if (permutation.packageIntegrity === 'missing') {
packageIntegrities = [];
shouldSucceed = false;
} else if (permutation.packageIntegrity !== 'match') {
throw new Error('unreachable');
}
resources['./package.json'] = {
body: packageBody,
integrities: packageIntegrities,
};
if (permutation.onError === 'log') {
shouldSucceed = true;
}
tests.add(
JSON.stringify({
onError: permutation.onError,
shouldSucceed,
entryPath: workerSpawnerPath,
preloads: permutation.preloads
.map((_) => {
return {
'': '',
'parent': parentFormat === 'commonjs' ? parentPath : '',
'dep': depFormat === 'commonjs' ? depPath : '',
}[_];
})
.filter(Boolean),
parentPath,
depPath,
resources,
}),
);
}
debug(`spawning ${tests.size} policy integrity permutations`);
for (const config of tests) {
const parsed = JSON.parse(config);
queueSpawn(parsed);
}

View File

@ -1,373 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
if (common.isPi) {
common.skip('Too slow for Raspberry Pi devices');
}
common.requireNoPackageJSONAbove();
const { debuglog } = require('util');
const debug = debuglog('test');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync, spawn } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const cpus = require('os').availableParallelism();
function hash(algo, body) {
const values = [];
{
const h = crypto.createHash(algo);
h.update(body);
values.push(`${algo}-${h.digest('base64')}`);
}
{
const h = crypto.createHash(algo);
h.update(body.replace('\n', '\r\n'));
values.push(`${algo}-${h.digest('base64')}`);
}
return values;
}
const policyPath = './policy.json';
const parentBody = {
commonjs: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
require(process.env.DEP_FILE)
`,
module: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
import(process.env.DEP_FILE)
`,
};
const workerSpawningBody = `
const path = require('path');
const { Worker } = require('worker_threads');
if (!process.env.PARENT_FILE) {
console.error(
'missing required PARENT_FILE env to determine worker entry point'
);
process.exit(33);
}
if (!process.env.DELETABLE_POLICY_FILE) {
console.error(
'missing required DELETABLE_POLICY_FILE env to check reloading'
);
process.exit(33);
}
const w = new Worker(path.resolve(process.env.PARENT_FILE));
w.on('exit', (status) => process.exit(status === 0 ? 0 : 1));
`;
let nextTestId = 1;
function newTestId() {
return nextTestId++;
}
tmpdir.refresh();
common.requireNoPackageJSONAbove(tmpdir.path);
let spawned = 0;
const toSpawn = [];
function queueSpawn(opts) {
toSpawn.push(opts);
drainQueue();
}
function drainQueue() {
if (spawned > cpus) {
return;
}
if (toSpawn.length) {
const config = toSpawn.shift();
const {
shouldSucceed,
preloads,
entryPath,
onError,
resources,
parentPath,
depPath,
} = config;
const testId = newTestId();
const configDirPath = path.join(
tmpdir.path,
`test-policy-integrity-permutation-${testId}`,
);
const tmpPolicyPath = path.join(
tmpdir.path,
`deletable-policy-${testId}.json`,
);
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
fs.mkdirSync(configDirPath, { recursive: true });
const manifest = {
onerror: onError,
resources: {},
};
const manifestPath = path.join(configDirPath, policyPath);
for (const [resourcePath, { body, integrities }] of Object.entries(
resources,
)) {
const filePath = path.join(configDirPath, resourcePath);
if (integrities !== null) {
manifest.resources[pathToFileURL(filePath).href] = {
integrity: integrities.join(' '),
dependencies: true,
};
}
fs.writeFileSync(filePath, body, 'utf8');
}
const manifestBody = JSON.stringify(manifest);
fs.writeFileSync(manifestPath, manifestBody);
fs.writeFileSync(tmpPolicyPath, manifestBody);
const spawnArgs = [
process.execPath,
[
'--unhandled-rejections=strict',
'--experimental-policy',
tmpPolicyPath,
...preloads.flatMap((m) => ['-r', m]),
entryPath,
'--',
testId,
configDirPath,
],
{
env: {
...process.env,
DELETABLE_POLICY_FILE: tmpPolicyPath,
PARENT_FILE: parentPath,
DEP_FILE: depPath,
},
cwd: configDirPath,
stdio: 'pipe',
},
];
spawned++;
const stdout = [];
const stderr = [];
const child = spawn(...spawnArgs);
child.stdout.on('data', (d) => stdout.push(d));
child.stderr.on('data', (d) => stderr.push(d));
child.on('exit', (status, signal) => {
spawned--;
try {
if (shouldSucceed) {
assert.strictEqual(status, 0);
} else {
assert.notStrictEqual(status, 0);
}
} catch (e) {
console.log(
'permutation',
testId,
'failed',
);
console.dir(
{ config, manifest },
{ depth: null },
);
console.log('exit code:', status, 'signal:', signal);
console.log(`stdout: ${Buffer.concat(stdout)}`);
console.log(`stderr: ${Buffer.concat(stderr)}`);
throw e;
}
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
drainQueue();
});
}
}
{
const { status } = spawnSync(
process.execPath,
['--experimental-policy', policyPath, '--experimental-policy', policyPath],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
}
{
const enoentFilepath = tmpdir.resolve('enoent');
try {
fs.unlinkSync(enoentFilepath);
} catch {
// Continue regardless of error.
}
const { status } = spawnSync(
process.execPath,
['--experimental-policy', enoentFilepath, '-e', ''],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow missing policies');
}
/**
* @template {Record<string, Array<string | string[] | boolean>>} T
* @param {T} configurations
* @param {object} path
* @returns {Array<{[key: keyof T]: T[keyof configurations]}>}
*/
function permutations(configurations, path = {}) {
const keys = Object.keys(configurations);
if (keys.length === 0) {
return path;
}
const config = keys[0];
const { [config]: values, ...otherConfigs } = configurations;
return values.flatMap((value) => {
return permutations(otherConfigs, { ...path, [config]: value });
});
}
const tests = new Set();
function fileExtensionFormat(extension) {
if (extension === '.js') {
return 'module';
} else if (extension === '.mjs') {
return 'module';
} else if (extension === '.cjs') {
return 'commonjs';
}
throw new Error('unknown format ' + extension);
}
for (const permutation of permutations({
preloads: [[], ['parent'], ['dep']],
onError: ['log', 'exit'],
parentExtension: ['.js', '.mjs', '.cjs'],
parentIntegrity: ['match', 'invalid', 'missing'],
depExtension: ['.js', '.mjs', '.cjs'],
depIntegrity: ['match', 'invalid', 'missing'],
packageIntegrity: ['match', 'invalid', 'missing'],
})) {
let shouldSucceed = true;
const parentPath = `./parent${permutation.parentExtension}`;
const parentFormat = fileExtensionFormat(permutation.parentExtension);
const depFormat = fileExtensionFormat(permutation.depExtension);
// non-sensical attempt to require ESM
if (depFormat === 'module' && parentFormat === 'commonjs') {
continue;
}
const depPath = `./dep${permutation.depExtension}`;
const workerSpawnerPath = './worker-spawner.cjs';
const packageJSON = {
main: workerSpawnerPath,
type: 'module',
};
const resources = {
[depPath]: {
body: '',
integrities: hash('sha256', ''),
},
};
if (permutation.depIntegrity === 'invalid') {
resources[depPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.depIntegrity === 'missing') {
resources[depPath].integrities = null;
shouldSucceed = false;
} else if (permutation.depIntegrity !== 'match') {
throw new Error('unreachable');
}
if (parentFormat !== 'commonjs') {
permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent');
}
resources[parentPath] = {
body: parentBody[parentFormat],
integrities: hash('sha256', parentBody[parentFormat]),
};
if (permutation.parentIntegrity === 'invalid') {
resources[parentPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.parentIntegrity === 'missing') {
resources[parentPath].integrities = null;
shouldSucceed = false;
} else if (permutation.parentIntegrity !== 'match') {
throw new Error('unreachable');
}
resources[workerSpawnerPath] = {
body: workerSpawningBody,
integrities: hash('sha256', workerSpawningBody),
};
let packageBody = JSON.stringify(packageJSON, null, 2);
let packageIntegrities = hash('sha256', packageBody);
if (
permutation.parentExtension !== '.js' ||
permutation.depExtension !== '.js'
) {
// NO PACKAGE LOOKUP
continue;
}
if (permutation.packageIntegrity === 'invalid') {
packageJSON['//'] = 'INVALID INTEGRITY';
packageBody = JSON.stringify(packageJSON, null, 2);
shouldSucceed = false;
} else if (permutation.packageIntegrity === 'missing') {
packageIntegrities = [];
shouldSucceed = false;
} else if (permutation.packageIntegrity !== 'match') {
throw new Error('unreachable');
}
resources['./package.json'] = {
body: packageBody,
integrities: packageIntegrities,
};
if (permutation.onError === 'log') {
shouldSucceed = true;
}
tests.add(
JSON.stringify({
onError: permutation.onError,
shouldSucceed,
entryPath: workerSpawnerPath,
preloads: permutation.preloads
.map((_) => {
return {
'': '',
'parent': parentFormat === 'commonjs' ? parentPath : '',
'dep': depFormat === 'commonjs' ? depPath : '',
}[_];
})
.filter(Boolean),
parentPath,
depPath,
resources,
}),
);
}
debug(`spawning ${tests.size} policy integrity permutations`);
for (const config of tests) {
const parsed = JSON.parse(config);
queueSpawn(parsed);
}

View File

@ -1,345 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
if (common.isPi) {
common.skip('Too slow for Raspberry Pi devices');
}
common.requireNoPackageJSONAbove();
const { debuglog } = require('util');
const debug = debuglog('test');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync, spawn } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const cpus = require('os').availableParallelism();
function hash(algo, body) {
const values = [];
{
const h = crypto.createHash(algo);
h.update(body);
values.push(`${algo}-${h.digest('base64')}`);
}
{
const h = crypto.createHash(algo);
h.update(body.replace('\n', '\r\n'));
values.push(`${algo}-${h.digest('base64')}`);
}
return values;
}
const policyPath = './policy.json';
const parentBody = {
commonjs: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
require(process.env.DEP_FILE)
`,
module: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
import(process.env.DEP_FILE)
`,
};
const workerSpawningBody = `
const path = require('path');
const { Worker } = require('worker_threads');
if (!process.env.PARENT_FILE) {
console.error(
'missing required PARENT_FILE env to determine worker entry point'
);
process.exit(33);
}
if (!process.env.DELETABLE_POLICY_FILE) {
console.error(
'missing required DELETABLE_POLICY_FILE env to check reloading'
);
process.exit(33);
}
const w = new Worker(path.resolve(process.env.PARENT_FILE));
w.on('exit', (status) => process.exit(status === 0 ? 0 : 1));
`;
let nextTestId = 1;
function newTestId() {
return nextTestId++;
}
tmpdir.refresh();
common.requireNoPackageJSONAbove(tmpdir.path);
let spawned = 0;
const toSpawn = [];
function queueSpawn(opts) {
toSpawn.push(opts);
drainQueue();
}
function drainQueue() {
if (spawned > cpus) {
return;
}
if (toSpawn.length) {
const config = toSpawn.shift();
const {
shouldSucceed,
preloads,
entryPath,
onError,
resources,
parentPath,
depPath,
} = config;
const testId = newTestId();
const configDirPath = path.join(
tmpdir.path,
`test-policy-integrity-permutation-${testId}`,
);
const tmpPolicyPath = path.join(
tmpdir.path,
`deletable-policy-${testId}.json`,
);
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
fs.mkdirSync(configDirPath, { recursive: true });
const manifest = {
onerror: onError,
resources: {},
};
const manifestPath = path.join(configDirPath, policyPath);
for (const [resourcePath, { body, integrities }] of Object.entries(
resources,
)) {
const filePath = path.join(configDirPath, resourcePath);
if (integrities !== null) {
manifest.resources[pathToFileURL(filePath).href] = {
integrity: integrities.join(' '),
dependencies: true,
};
}
fs.writeFileSync(filePath, body, 'utf8');
}
const manifestBody = JSON.stringify(manifest);
fs.writeFileSync(manifestPath, manifestBody);
fs.writeFileSync(tmpPolicyPath, manifestBody);
const spawnArgs = [
process.execPath,
[
'--unhandled-rejections=strict',
'--experimental-policy',
tmpPolicyPath,
...preloads.flatMap((m) => ['-r', m]),
entryPath,
'--',
testId,
configDirPath,
],
{
env: {
...process.env,
DELETABLE_POLICY_FILE: tmpPolicyPath,
PARENT_FILE: parentPath,
DEP_FILE: depPath,
},
cwd: configDirPath,
stdio: 'pipe',
},
];
spawned++;
const stdout = [];
const stderr = [];
const child = spawn(...spawnArgs);
child.stdout.on('data', (d) => stdout.push(d));
child.stderr.on('data', (d) => stderr.push(d));
child.on('exit', (status, signal) => {
spawned--;
try {
if (shouldSucceed) {
assert.strictEqual(status, 0);
} else {
assert.notStrictEqual(status, 0);
}
} catch (e) {
console.log(
'permutation',
testId,
'failed',
);
console.dir(
{ config, manifest },
{ depth: null },
);
console.log('exit code:', status, 'signal:', signal);
console.log(`stdout: ${Buffer.concat(stdout)}`);
console.log(`stderr: ${Buffer.concat(stderr)}`);
throw e;
}
fs.rmSync(configDirPath, { maxRetries: 3, recursive: true, force: true });
drainQueue();
});
}
}
{
const { status } = spawnSync(
process.execPath,
['--experimental-policy', policyPath, '--experimental-policy', policyPath],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
}
{
const enoentFilepath = tmpdir.resolve('enoent');
try {
fs.unlinkSync(enoentFilepath);
} catch {
// Continue regardless of error.
}
const { status } = spawnSync(
process.execPath,
['--experimental-policy', enoentFilepath, '-e', ''],
{
stdio: 'pipe',
},
);
assert.notStrictEqual(status, 0, 'Should not allow missing policies');
}
/**
* @template {Record<string, Array<string | string[] | boolean>>} T
* @param {T} configurations
* @param {object} path
* @returns {Array<{[key: keyof T]: T[keyof configurations]}>}
*/
function permutations(configurations, path = {}) {
const keys = Object.keys(configurations);
if (keys.length === 0) {
return path;
}
const config = keys[0];
const { [config]: values, ...otherConfigs } = configurations;
return values.flatMap((value) => {
return permutations(otherConfigs, { ...path, [config]: value });
});
}
const tests = new Set();
function fileExtensionFormat(extension) {
if (extension === '.js') {
return 'commonjs';
} else if (extension === '.mjs') {
return 'module';
} else if (extension === '.cjs') {
return 'commonjs';
}
throw new Error('unknown format ' + extension);
}
for (const permutation of permutations({
preloads: [[], ['parent'], ['dep']],
onError: ['log', 'exit'],
parentExtension: ['.js', '.mjs', '.cjs'],
parentIntegrity: ['match', 'invalid', 'missing'],
depExtension: ['.js', '.mjs', '.cjs'],
depIntegrity: ['match', 'invalid', 'missing'],
packageIntegrity: ['match', 'invalid', 'missing'],
})) {
let shouldSucceed = true;
const parentPath = `./parent${permutation.parentExtension}`;
const parentFormat = fileExtensionFormat(permutation.parentExtension);
const depFormat = fileExtensionFormat(permutation.depExtension);
// non-sensical attempt to require ESM
if (depFormat === 'module' && parentFormat === 'commonjs') {
continue;
}
const depPath = `./dep${permutation.depExtension}`;
const workerSpawnerPath = './worker-spawner.cjs';
const resources = {
[depPath]: {
body: '',
integrities: hash('sha256', ''),
},
};
if (permutation.depIntegrity === 'invalid') {
resources[depPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.depIntegrity === 'missing') {
resources[depPath].integrities = null;
shouldSucceed = false;
} else if (permutation.depIntegrity !== 'match') {
throw new Error('unreachable');
}
if (parentFormat !== 'commonjs') {
permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent');
}
resources[parentPath] = {
body: parentBody[parentFormat],
integrities: hash('sha256', parentBody[parentFormat]),
};
if (permutation.parentIntegrity === 'invalid') {
resources[parentPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.parentIntegrity === 'missing') {
resources[parentPath].integrities = null;
shouldSucceed = false;
} else if (permutation.parentIntegrity !== 'match') {
throw new Error('unreachable');
}
resources[workerSpawnerPath] = {
body: workerSpawningBody,
integrities: hash('sha256', workerSpawningBody),
};
if (permutation.onError === 'log') {
shouldSucceed = true;
}
tests.add(
JSON.stringify({
onError: permutation.onError,
shouldSucceed,
entryPath: workerSpawnerPath,
preloads: permutation.preloads
.map((_) => {
return {
'': '',
'parent': parentFormat === 'commonjs' ? parentPath : '',
'dep': depFormat === 'commonjs' ? depPath : '',
}[_];
})
.filter(Boolean),
parentPath,
depPath,
resources,
}),
);
}
debug(`spawning ${tests.size} policy integrity permutations`);
for (const config of tests) {
const parsed = JSON.parse(config);
queueSpawn(parsed);
}

View File

@ -20,11 +20,7 @@ export type SerializedPackageConfig = [
export interface ModulesBinding {
readPackageJSON(path: string): SerializedPackageConfig | undefined;
getNearestParentPackageJSON(path: string): PackageConfig | undefined
getNearestParentPackageJSONType(path: string): [
PackageConfig['type'],
string, // package.json path
string, // raw content
]
getNearestParentPackageJSONType(path: string): PackageConfig['type']
getPackageScopeConfig(path: string): SerializedPackageConfig | undefined
getPackageJSONScripts(): string | undefined
}