From 18b04ab4c850a1c45e6817eb159b52b339435805 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Fri, 17 Jul 2020 11:38:46 -0500 Subject: [PATCH] policy: implement scopes field PR-URL: https://github.com/nodejs/node/pull/34552 Reviewed-By: James M Snell --- doc/api/policy.md | 89 +++ lib/internal/modules/cjs/loader.js | 21 +- lib/internal/modules/esm/get_source.js | 9 +- lib/internal/modules/esm/resolve.js | 9 +- lib/internal/modules/package_json_reader.js | 12 +- lib/internal/policy/manifest.js | 528 +++++++++++++----- lib/internal/process/execution.js | 49 +- lib/internal/util/compositekey.js | 114 ---- node.gyp | 1 - .../dependencies-scopes-policy.json | 8 + test/fixtures/policy/main.mjs | 2 + test/message/eval_messages.out | 16 +- test/message/stdin_messages.out | 16 +- .../test-policy-dependency-conditions.js | 2 +- .../test-policy-scopes-dependencies.js | 306 ++++++++++ test/parallel/test-policy-scopes-integrity.js | 282 ++++++++++ test/parallel/test-policy-scopes.js | 25 + ...spector-async-hook-setup-at-inspect-brk.js | 2 +- ...spector-async-stack-traces-set-interval.js | 2 +- 19 files changed, 1154 insertions(+), 339 deletions(-) delete mode 100644 lib/internal/util/compositekey.js create mode 100644 test/fixtures/policy/dependencies/dependencies-scopes-policy.json create mode 100644 test/fixtures/policy/main.mjs create mode 100644 test/parallel/test-policy-scopes-dependencies.js create mode 100644 test/parallel/test-policy-scopes-integrity.js create mode 100644 test/parallel/test-policy-scopes.js diff --git a/doc/api/policy.md b/doc/api/policy.md index e5387f49c0306d..4af900f2e9b406 100644 --- a/doc/api/policy.md +++ b/doc/api/policy.md @@ -196,4 +196,93 @@ module.exports = function fn(...args) { }; ``` +### Scopes + +Use the `"scopes"` field of a manifest to set configuration for many resources +at once. The `"scopes"` field works by matching resources by their segments. +If a scope or resource includes `"cascade": true` unknown specifiers will +be searched for in their containing scope. The containing scope for cascading +is found by recursively reducing the resource URL by removing segments for +[special schemes][], keeping trailing `"/"` suffixes and removing the query and +hash fragment. This leads to the eventual reduction of the URL to its origin. +If the URL is non-special the scope will be located by the URL's origin. If no +scope is found for the origin or in the case of opaque origins, a protocol +string can be used as a scope. + +Note, `blob:` URLs adopt their origin from the path they contain, and so a scope +of `"blob:https://nodejs.org"` will have no effect since no URL can have an +origin of `blob:https://nodejs.org`; URLs starting with +`blob:https://nodejs.org/` will use `https://nodejs.org` for its origin and +thus `https:` for its protocol scope. For opaque origin `blob:` URLs they will +have `blob:` for their protocol scope since they do not adopt origins. + +#### Integrity Using Scopes + +Setting an integrity to `true` on a scope will set the integrity for any +resource not found in the manifest to `true`. + +Setting an integrity to `null` on a scope will set the integrity for any +resource not found in the manifest to fail matching. + +Not including an integrity is the same as setting the integrity to `null`. + +`"cascade"` for integrity checks will be ignored if `"integrity"` is explicitly +set. + +The following example allows loading any file: + +```json +{ + "scopes": { + "file:": { + "integrity": true + } + } +} +``` + +#### Dependency Redirection Using Scopes + +The following example, would allow access to `fs` for all resources within +`./app/`: + +```json +{ + "resources": { + "./app/checked.js": { + "cascade": true, + "integrity": true + } + }, + "scopes": { + "./app/": { + "dependencies": { + "fs": true + } + } + } +} +``` + +The following example, would allow access to `fs` for all `data:` resources: + +```json +{ + "resources": { + "data:text/javascript,import('fs');": { + "cascade": true, + "integrity": true + } + }, + "scopes": { + "data:": { + "dependencies": { + "fs": true + } + } + } +} +``` + [relative url string]: https://url.spec.whatwg.org/#relative-url-with-fragment-string +[special schemes]: https://url.spec.whatwg.org/#special-scheme diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index a7fa93b9cfa946..0f8647a5f22992 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -46,8 +46,8 @@ const { SafeMap, String, StringPrototypeEndsWith, - StringPrototypeIndexOf, StringPrototypeLastIndexOf, + StringPrototypeIndexOf, StringPrototypeMatch, StringPrototypeSlice, StringPrototypeStartsWith, @@ -81,8 +81,9 @@ const { getOptionValue } = require('internal/options'); const enableSourceMaps = getOptionValue('--enable-source-maps'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); -const manifest = getOptionValue('--experimental-policy') ? - require('internal/process/policy').manifest : +// Do not eagerly grab .manifest, it may be in TDZ +const policy = getOptionValue('--experimental-policy') ? + require('internal/process/policy') : null; const { compileFunction } = internalBinding('contextify'); @@ -1029,10 +1030,10 @@ function wrapSafe(filename, content, cjsModuleInstance) { Module.prototype._compile = function(content, filename) { let moduleURL; let redirects; - if (manifest) { + if (policy?.manifest) { moduleURL = pathToFileURL(filename); - redirects = manifest.getRedirector(moduleURL); - manifest.assertIntegrity(moduleURL, content); + redirects = policy.manifest.getDependencyMapper(moduleURL); + policy.manifest.assertIntegrity(moduleURL, content); } maybeCacheSourceMap(filename, content, this); @@ -1101,9 +1102,9 @@ Module._extensions['.js'] = function(module, filename) { Module._extensions['.json'] = function(module, filename) { const content = fs.readFileSync(filename, 'utf8'); - if (manifest) { + if (policy?.manifest) { const moduleURL = pathToFileURL(filename); - manifest.assertIntegrity(moduleURL, content); + policy.manifest.assertIntegrity(moduleURL, content); } try { @@ -1117,10 +1118,10 @@ Module._extensions['.json'] = function(module, filename) { // Native extension for .node Module._extensions['.node'] = function(module, filename) { - if (manifest) { + if (policy?.manifest) { const content = fs.readFileSync(filename); const moduleURL = pathToFileURL(filename); - manifest.assertIntegrity(moduleURL, content); + policy.manifest.assertIntegrity(moduleURL, content); } // Be aware this doesn't use `content` return process.dlopen(module, path.toNamespacedPath(filename)); diff --git a/lib/internal/modules/esm/get_source.js b/lib/internal/modules/esm/get_source.js index 5e2cc3e09e7687..54dbd029fcfc22 100644 --- a/lib/internal/modules/esm/get_source.js +++ b/lib/internal/modules/esm/get_source.js @@ -1,8 +1,9 @@ 'use strict'; const { getOptionValue } = require('internal/options'); -const manifest = getOptionValue('--experimental-policy') ? - require('internal/process/policy').manifest : +// Do not eagerly grab .manifest, it may be in TDZ +const policy = getOptionValue('--experimental-policy') ? + require('internal/process/policy') : null; const { Buffer } = require('buffer'); @@ -33,8 +34,8 @@ async function defaultGetSource(url, { format } = {}, defaultGetSource) { } else { throw new ERR_INVALID_URL_SCHEME(['file', 'data']); } - if (manifest) { - manifest.assertIntegrity(parsed, source); + if (policy?.manifest) { + policy.manifest.assertIntegrity(parsed, source); } return { source }; } diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index d58eac5f1c10cb..0a828867692473 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -30,8 +30,9 @@ const { Stats, } = require('fs'); const { getOptionValue } = require('internal/options'); -const manifest = getOptionValue('--experimental-policy') ? - require('internal/process/policy').manifest : +// Do not eagerly grab .manifest, it may be in TDZ +const policy = getOptionValue('--experimental-policy') ? + require('internal/process/policy') : null; const { sep, relative } = require('path'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); @@ -714,8 +715,8 @@ function resolveAsCommonJS(specifier, parentURL) { function defaultResolve(specifier, context = {}, defaultResolveUnused) { let { parentURL, conditions } = context; - if (manifest) { - const redirects = manifest.getRedirector(parentURL); + if (parentURL && policy?.manifest) { + const redirects = policy.manifest.getDependencyMapper(parentURL); if (redirects) { const { resolve, reaction } = redirects; const destination = resolve(specifier, new SafeSet(conditions)); diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js index 25edfee027c35b..4a2b0e6ddb3ed8 100644 --- a/lib/internal/modules/package_json_reader.js +++ b/lib/internal/modules/package_json_reader.js @@ -7,6 +7,8 @@ const { toNamespacedPath } = require('path'); const cache = new SafeMap(); +let manifest; + /** * * @param {string} jsonPath @@ -22,10 +24,12 @@ function read(jsonPath) { const result = { string, containsKeys }; const { getOptionValue } = require('internal/options'); if (string !== undefined) { - const manifest = getOptionValue('--experimental-policy') ? - require('internal/process/policy').manifest : - null; - if (manifest) { + if (manifest === undefined) { + manifest = getOptionValue('--experimental-policy') ? + require('internal/process/policy').manifest : + null; + } + if (manifest !== null) { const jsonURL = pathToFileURL(jsonPath); manifest.assertIntegrity(jsonURL, string); } diff --git a/lib/internal/policy/manifest.js b/lib/internal/policy/manifest.js index 920790a22ed46e..71dc3d78ab09e4 100644 --- a/lib/internal/policy/manifest.js +++ b/lib/internal/policy/manifest.js @@ -11,15 +11,13 @@ const { ObjectSetPrototypeOf, RegExpPrototypeTest, SafeMap, + SafeSet, + Symbol, uncurryThis, } = primordials; -const { - compositeKey -} = require('internal/util/compositekey'); const { canBeRequiredByUsers } = require('internal/bootstrap/loaders').NativeModule; - const { ERR_MANIFEST_ASSERT_INTEGRITY, ERR_MANIFEST_INTEGRITY_MISMATCH, @@ -43,6 +41,18 @@ const shouldAbortOnUncaughtException = getOptionValue('--abort-on-uncaught-exception'); const { abort, exit, _rawDebug } = process; +// From https://url.spec.whatwg.org/#special-scheme +const SPECIAL_SCHEMES = new SafeSet([ + 'file:', + 'ftp:', + 'http:', + 'https:', + 'ws:', + 'wss:', +]); + +const kCascade = Symbol('cascade'); + function REACTION_THROW(error) { throw error; } @@ -58,10 +68,33 @@ function REACTION_EXIT(error) { function REACTION_LOG(error) { _rawDebug(error.stack); } +/** + * @typedef {(specifier: string) => true | URL} DependencyMapper + * @typedef {Record | true} DependencyMap + * @typedef {true | string | SRI[]} Integrity + */ class Manifest { /** - * @type {Map} + * @type {Map} + * + * 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} + * + * Used to allow arbitrary loading within a scope + */ + #scopeIntegrities = new SafeMap(); + /** + * @type {Map} * * Used to compare a resource to the content body at the resource. * `true` is used to signify that all integrities are allowed, otherwise, @@ -72,14 +105,9 @@ class Manifest { * This avoids needing to parse all SRI strings at startup even * if some never end up being used. */ - #integrities = new SafeMap(); + #resourceIntegrities = new SafeMap(); /** - * @type { - Map< - string, - (specifier: string, conditions: Set) => true | null | URL - > - } + * @type {Map} * * Used to find where a dependency is located. * @@ -90,7 +118,7 @@ class Manifest { * The functions return `null` to signify that a dependency is * not found */ - #dependencies = new SafeMap(); + #resourceDependencies = new SafeMap(); /** * @type {(err: Error) => void} * @@ -110,8 +138,9 @@ class Manifest { * @param {string} manifestURL */ constructor(obj, manifestURL) { - const integrities = this.#integrities; - const dependencies = this.#dependencies; + const scopes = this.#scopeDependencies; + const integrities = this.#resourceIntegrities; + const dependencies = this.#resourceDependencies; let reaction = REACTION_THROW; if (obj.onerror) { @@ -127,34 +156,207 @@ class Manifest { } this.#reaction = reaction; - const manifestEntries = ObjectEntries(obj.resources); + const jsonResourcesEntries = ObjectEntries(obj.resources ?? {}); + const jsonScopesEntries = ObjectEntries(obj.scopes ?? {}); + + function searchDependencies(resourceHREF, target, conditions) { + if ( + target && + typeof target === 'object' && + !ArrayIsArray(target) + ) { + const keys = ObjectKeys(target); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (conditions.has(key)) { + const ret = searchDependencies( + resourceHREF, + 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( + resourceHREF, + 'dependencies'); + } + return null; + } + + /** + * @param {string} resourceHREF + * @param {{[key: string]: string | true}} dependencyMap + * @param {boolean} cascade + * @returns {DependencyMapper} + */ + const createDependencyMapper = ( + resourceHREF, + dependencyMap, + cascade + ) => { + let parentDeps; + return (toSpecifier, conditions) => { + if (toSpecifier in dependencyMap !== true) { + if (cascade === true) { + let scopeHREF; + if (typeof parentDeps === 'undefined') { + do { + scopeHREF = this.#findScopeHREF(resourceHREF); + } while ( + scopeHREF !== null && + scopes.has(scopeHREF) !== true + ); + } + if (scopeHREF === null) { + parentDeps = () => null; + } else { + parentDeps = scopes.get(scopeHREF); + } + return parentDeps(toSpecifier); + } + return null; + } + const to = searchDependencies( + resourceHREF, + dependencyMap[toSpecifier], + conditions); + if (to === true) { + return true; + } + if (parsedURLs.has(to)) { + return parsedURLs.get(to); + } else if (canBeRequiredByUsers(to)) { + const href = `node:${to}`; + const resolvedURL = new URL(href); + parsedURLs.set(to, resolvedURL); + parsedURLs.set(href, resolvedURL); + return resolvedURL; + } else if (RegExpPrototypeTest(kRelativeURLStringPattern, to)) { + const resolvedURL = new URL(to, manifestURL); + const href = resourceHREF; + parsedURLs.set(to, resolvedURL); + parsedURLs.set(href, resolvedURL); + return resolvedURL; + } + const resolvedURL = new URL(to); + const href = resourceHREF; + parsedURLs.set(to, resolvedURL); + parsedURLs.set(href, resolvedURL); + return resolvedURL; + }; + }; + /** + * Stores URLs keyed by string specifier relative to the manifest + * @type {Map} + */ const parsedURLs = new SafeMap(); - for (let i = 0; i < manifestEntries.length; i++) { - let resourceHREF = manifestEntries[i][0]; - const originalHREF = resourceHREF; - let resourceURL; - if (parsedURLs.has(resourceHREF)) { - resourceURL = parsedURLs.get(resourceHREF); - resourceHREF = resourceURL.href; + + /** + * Resolves a valid url string against the manifest + * @param {string} originalHREF + * @returns {string} + */ + const resolve = (originalHREF) => { + if (parsedURLs.has(originalHREF)) { + return parsedURLs.get(originalHREF).href; } else if ( - RegExpPrototypeTest(kRelativeURLStringPattern, resourceHREF) + RegExpPrototypeTest(kRelativeURLStringPattern, originalHREF) ) { - resourceURL = new URL(resourceHREF, manifestURL); - resourceHREF = resourceURL.href; + const resourceURL = new URL(originalHREF, manifestURL); + const resourceHREF = resourceURL.href; parsedURLs.set(originalHREF, resourceURL); - parsedURLs.set(resourceHREF, resourceURL); + parsedURLs.set(resourceURL.href, resourceURL); + return resourceHREF; } - let integrity = manifestEntries[i][1].integrity; - if (!integrity) integrity = null; - if (integrity != null) { - debug('Manifest contains integrity for url %s', originalHREF); - if (typeof integrity === 'string') { - if (integrities.has(resourceHREF)) { - if (integrities.get(resourceHREF) !== integrity) { - throw new ERR_MANIFEST_INTEGRITY_MISMATCH(resourceURL); - } + const resourceURL = new URL(originalHREF); + const resourceHREF = resourceURL.href; + parsedURLs.set(originalHREF, resourceURL); + return resourceHREF; + }; + + /** + * @param {string} resourceHREF + * @param {DependencyMap} dependencyMap + * @param {boolean} cascade + * @param {Map} store + */ + const insertDependencyMap = ( + resourceHREF, + dependencyMap, + cascade, + store + ) => { + if (cascade !== undefined && typeof cascade !== 'boolean') { + throw new ERR_MANIFEST_INVALID_RESOURCE_FIELD( + resourceHREF, + 'cascade'); + } + if (dependencyMap === null || dependencyMap === undefined) { + dependencyMap = ObjectCreate(null); + } + if ( + typeof dependencyMap === 'object' && + !ArrayIsArray(dependencyMap) + ) { + const dependencyRedirectList = createDependencyMapper( + resourceHREF, + dependencyMap, + cascade); + store.set(resourceHREF, dependencyRedirectList); + return; + } else if (dependencyMap === true) { + const arbitraryDependencies = /** @type {()=>true} */() => true; + store.set(resourceHREF, arbitraryDependencies); + return; + } + throw new ERR_MANIFEST_INVALID_RESOURCE_FIELD( + resourceHREF, + 'dependencies'); + }; + /** + * Does a special allowance for scopes to be non-valid URLs + * that are only protocol strings + * @param {string} resourceHREF + * @returns {string} + */ + const protocolOrResolve = (resourceHREF) => { + if (resourceHREF.endsWith(':')) { + // URL parse will trim these anyway, save the compute + resourceHREF = resourceHREF.replace( + // eslint-disable-next-line + /^[\x00-\x1F\x20]|\x09\x0A\x0D|[\x00-\x1F\x20]$/g, + '' + ); + if (/^[a-zA-Z][a-zA-Z+\-.]*:$/.test(resourceHREF)) { + return resourceHREF; + } + } + return resolve(resourceHREF); + }; + + for (let i = 0; i < jsonResourcesEntries.length; i++) { + const [originalHREF, resourceDescriptor] = jsonResourcesEntries[i]; + const cascade = resourceDescriptor.cascade; + const dependencyMap = resourceDescriptor.dependencies; + const resourceHREF = resolve(originalHREF); + + const integrity = resourceDescriptor.integrity; + if (typeof integrity !== 'undefined') { + debug('Manifest contains integrity for resource %s', originalHREF); + if (integrities.has(resourceHREF)) { + if (integrities.get(resourceHREF) !== integrity) { + throw new ERR_MANIFEST_INTEGRITY_MISMATCH(resourceHREF); } + } + if (typeof integrity === 'string') { integrities.set(resourceHREF, integrity); } else if (integrity === true) { integrities.set(resourceHREF, true); @@ -163,127 +365,114 @@ class Manifest { resourceHREF, 'integrity'); } + } else { + integrities.set(resourceHREF, cascade ? kCascade : null); } + insertDependencyMap(resourceHREF, dependencyMap, cascade, dependencies); + } - let dependencyMap = manifestEntries[i][1].dependencies; - if (dependencyMap === null || dependencyMap === undefined) { - dependencyMap = ObjectCreate(null); - } - if (typeof dependencyMap === 'object' && !ArrayIsArray(dependencyMap)) { - function searchDependencies(target, conditions) { - if ( - target && - typeof target === 'object' && - !ArrayIsArray(target) - ) { - const keys = ObjectKeys(target); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - if (conditions.has(key)) { - const ret = searchDependencies(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( - resourceHREF, - 'dependencies'); + const scopeIntegrities = this.#scopeIntegrities; + for (let i = 0; i < jsonScopesEntries.length; i++) { + const [originalHREF, scopeDescriptor] = jsonScopesEntries[i]; + const integrity = scopeDescriptor.integrity; + const cascade = scopeDescriptor.cascade; + const dependencyMap = scopeDescriptor.dependencies; + const resourceHREF = protocolOrResolve(originalHREF); + if (typeof integrity !== 'undefined') { + debug('Manifest contains integrity for scope %s', originalHREF); + if (scopeIntegrities.has(resourceHREF)) { + if (scopeIntegrities.get(resourceHREF) !== integrity) { + throw new ERR_MANIFEST_INTEGRITY_MISMATCH(resourceHREF); } - return null; } - // This is used so we don't traverse this every time - // in theory we can delete parts of the dep map once this is populated - const localMappings = new SafeMap(); - /** - * @returns {true | null | URL} - */ - const dependencyRedirectList = (specifier, conditions) => { - const key = compositeKey([localMappings, specifier, ...conditions]); - if (localMappings.has(key)) { - return localMappings.get(key); - } - if (specifier in dependencyMap !== true) { - localMappings.set(key, null); - return null; - } - const target = searchDependencies( - dependencyMap[specifier], - conditions); - if (target === true) { - localMappings.set(key, true); - return true; - } - if (typeof target !== 'string') { - localMappings.set(key, null); - return null; - } - if (parsedURLs.has(target)) { - const parsed = parsedURLs.get(target); - localMappings.set(key, parsed); - return parsed; - } else if (canBeRequiredByUsers(target)) { - const href = `node:${target}`; - const resolvedURL = new URL(href); - parsedURLs.set(target, resolvedURL); - parsedURLs.set(href, resolvedURL); - localMappings.set(key, resolvedURL); - return resolvedURL; - } else if (RegExpPrototypeTest(kRelativeURLStringPattern, target)) { - const resolvedURL = new URL(target, manifestURL); - const href = resourceURL.href; - parsedURLs.set(target, resolvedURL); - parsedURLs.set(href, resolvedURL); - localMappings.set(key, resolvedURL); - return resolvedURL; - } - const resolvedURL = new URL(target); - const href = resolvedURL.href; - parsedURLs.set(target, resolvedURL); - parsedURLs.set(href, resolvedURL); - localMappings.set(key, resolvedURL); - return resolvedURL; - }; - dependencies.set(resourceHREF, dependencyRedirectList); - } else if (dependencyMap === true) { - const arbitraryDependencies = () => true; - dependencies.set(resourceHREF, arbitraryDependencies); + if (integrity === true) { + scopeIntegrities.set(resourceHREF, true); + } else { + throw new ERR_MANIFEST_INVALID_RESOURCE_FIELD( + resourceHREF, + 'integrity'); + } } else { - throw new ERR_MANIFEST_INVALID_RESOURCE_FIELD( - resourceHREF, - 'dependencies'); + scopeIntegrities.set(resourceHREF, cascade ? kCascade : null); } + insertDependencyMap(resourceHREF, dependencyMap, cascade, scopes); } + ObjectFreeze(this); } - getRedirector(requester) { - requester = `${requester}`; - const dependencies = this.#dependencies; - if (dependencies.has(requester)) { - return { - resolve: (specifier, conditions) => dependencies.get(requester)( - `${specifier}`, - conditions - ), - reaction: this.#reaction - }; + /** + * Finds the longest key within `this.#scopeDependencies` that covers a + * specific HREF + * @param {string} href + * @returns {null | string} + */ + #findScopeHREF = (href) => { + let currentURL = new URL(href); + let 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 (SPECIAL_SCHEMES.has(currentURL.protocol)) { + // Make first '..' act like '.' + if (currentURL.pathname.slice(-1) !== '/') { + currentURL.pathname += '/'; + } + let lastHREF; + let currentHREF = currentURL.href; + do { + if (this.#scopeDependencies.has(currentHREF)) { + return currentHREF; + } + lastHREF = currentHREF; + currentURL = new URL('..', currentURL); + currentHREF = currentURL.href; + } while (lastHREF !== currentHREF); + } + if (this.#scopeDependencies.has(protocol)) { + return protocol; } return null; } + #createResolver = (resolve) => { + return { + resolve: (to, conditions) => resolve(`${to}`, conditions), + reaction: this.#reaction + }; + } + + /** + * @param {string} requester + */ + getDependencyMapper(requester) { + const requesterHREF = `${requester}`; + const dependencies = this.#resourceDependencies; + if (dependencies.has(requesterHREF)) { + return this.#createResolver( + dependencies.get(requesterHREF) || + (() => null) + ); + } + const scopes = this.#scopeDependencies; + if (scopes.size !== 0) { + const scopeHREF = this.#findScopeHREF(requesterHREF); + if (typeof scopeHREF === 'string') { + return this.#createResolver(scopes.get(scopeHREF)); + } + } + return this.#createResolver(() => null); + } + assertIntegrity(url, content) { const href = `${url}`; debug('Checking integrity of %s', href); - const integrities = this.#integrities; const realIntegrities = new Map(); - - if (integrities.has(href)) { + const integrities = this.#resourceIntegrities; + function processEntry(href) { let integrityEntries = integrities.get(href); if (integrityEntries === true) return true; if (typeof integrityEntries === 'string') { @@ -291,25 +480,54 @@ class Manifest { integrities.set(href, sri); integrityEntries = sri; } - // Avoid clobbered Symbol.iterator - for (let i = 0; i < integrityEntries.length; i++) { - const { - algorithm, - value: expected - } = integrityEntries[i]; - const hash = createHash(algorithm); - HashUpdate(hash, content); - const digest = HashDigest(hash); - if (digest.length === expected.length && - timingSafeEqual(digest, expected)) { + 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]; + const hash = createHash(algorithm); + HashUpdate(hash, content); + const digest = HashDigest(hash); + if (digest.length === expected.length && + timingSafeEqual(digest, expected)) { + return true; + } + MapPrototypeSet( + realIntegrities, + algorithm, + BufferToString(digest, 'base64') + ); + } + } + + if (integrityEntries !== kCascade) { + const error = new ERR_MANIFEST_ASSERT_INTEGRITY(url, realIntegrities); + this.#reaction(error); + } + } + let scope = this.#findScopeHREF(href); + while (scope !== null) { + if (this.#scopeIntegrities.has(scope)) { + const entry = this.#scopeIntegrities.get(scope); + if (entry === true) { return true; + } else if (entry === kCascade) { + } else { + break; } - MapPrototypeSet( - realIntegrities, - algorithm, - BufferToString(digest, 'base64') - ); } + const nextScope = this.#findScopeHREF(new URL('..', scope)); + if (!nextScope || nextScope === scope) { + break; + } + scope = nextScope; } const error = new ERR_MANIFEST_ASSERT_INTEGRITY(url, realIntegrities); this.#reaction(error); diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index c948552453306b..1087afadc5d659 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -1,9 +1,5 @@ 'use strict'; -const { - JSONStringify, -} = primordials; - const path = require('path'); const { @@ -59,44 +55,41 @@ function evalScript(name, body, breakFirstLine, print) { const { pathToFileURL } = require('url'); const cwd = tryGetCwd(); - const origModule = global.module; // Set e.g. when called from the REPL. + const origModule = globalThis.module; // Set e.g. when called from the REPL. const module = new CJSModule(name); module.filename = path.join(cwd, name); module.paths = CJSModule._nodeModulePaths(cwd); - global.kVmBreakFirstLineSymbol = kVmBreakFirstLineSymbol; - global.asyncESM = require('internal/process/esm_loader'); - + const asyncESM = require('internal/process/esm_loader'); const baseUrl = pathToFileURL(module.filename).href; + // Create wrapper for cache entry const script = ` - global.__filename = ${JSONStringify(name)}; - global.exports = exports; - global.module = module; - global.__dirname = __dirname; - global.require = require; - const { kVmBreakFirstLineSymbol, asyncESM } = global; - delete global.kVmBreakFirstLineSymbol; - delete global.asyncESM; - return require("vm").runInThisContext( - ${JSONStringify(body)}, { - filename: ${JSONStringify(name)}, - displayErrors: true, - [kVmBreakFirstLineSymbol]: ${!!breakFirstLine}, - async importModuleDynamically (specifier) { - const loader = await asyncESM.ESMLoader; - return loader.import(specifier, ${JSONStringify(baseUrl)}); - } - });\n`; - const result = module._compile(script, `${name}-wrapper`); + globalThis.module = module; + globalThis.exports = exports; + globalThis.__dirname = __dirname; + globalThis.require = require; + return (main) => main(); + `; + globalThis.__filename = name; + const result = module._compile(script, `${name}-wrapper`)(() => + require('vm').runInThisContext(body, { + filename: name, + displayErrors: true, + [kVmBreakFirstLineSymbol]: !!breakFirstLine, + async importModuleDynamically(specifier) { + const loader = await asyncESM.ESMLoader; + return loader.import(specifier, baseUrl); + } + })); if (print) { const { log } = require('internal/console/global'); log(result); } if (origModule !== undefined) - global.module = origModule; + globalThis.module = origModule; } const exceptionHandlerState = { captureFn: null }; diff --git a/lib/internal/util/compositekey.js b/lib/internal/util/compositekey.js deleted file mode 100644 index 9b7c460231f876..00000000000000 --- a/lib/internal/util/compositekey.js +++ /dev/null @@ -1,114 +0,0 @@ -'use strict'; -const { - codes: { - ERR_INVALID_ARG_VALUE, - }, -} = require('internal/errors'); -const { - ObjectCreate, - ObjectFreeze, - SafeMap, - SafeWeakMap, -} = primordials; -/** - * @param {any} value - * @returns {boolean} - */ -const hasLifetime = (value) => { - return value !== null && ( - typeof value === 'object' || - typeof value === 'function' - ); -}; -class CompositeNode { - /** - * @type {WeakMap>} - */ - lifetimeNodes; - /** - * @type {Map>} - */ - primitiveNodes; - /** - * @type {null | Readonly<{}>} - */ - value; - constructor() { - this.value = null; - } - get() { - if (this.value === null) { - return this.value = ObjectFreeze(ObjectCreate(null)); - } - return this.value; - } - /** - * @param {any} value - * @param {number} position - */ - emplacePrimitive(value, position) { - if (!this.primitiveNodes) { - this.primitiveNodes = new SafeMap(); - } - if (!this.primitiveNodes.has(value)) { - this.primitiveNodes.set(value, new SafeMap()); - } - const positions = this.primitiveNodes.get(value); - if (!positions.has(position)) { - positions.set(position, new CompositeNode()); - } - return positions.get(position); - } - /** - * @param {object} value - * @param {number} position - */ - emplaceLifetime(value, position) { - if (!this.lifetimeNodes) { - this.lifetimeNodes = new SafeWeakMap(); - } - if (!this.lifetimeNodes.has(value)) { - this.lifetimeNodes.set(value, new SafeMap()); - } - const positions = this.lifetimeNodes.get(value); - if (!positions.has(position)) { - positions.set(position, new CompositeNode()); - } - return positions.get(position); - } -} -const compoundStore = new CompositeNode(); -// Accepts objects as a key and does identity on the parts of the iterable -/** - * @param {any[]} parts - */ -const compositeKey = (parts) => { - /** - * @type {CompositeNode} - */ - let node = compoundStore; - for (let i = 0; i < parts.length; i++) { - const value = parts[i]; - if (hasLifetime(value)) { - node = node.emplaceLifetime(value, i); - parts[i] = hasLifetime; - } - } - // Does not leak WeakMap paths since there are none added - if (node === compoundStore) { - throw new ERR_INVALID_ARG_VALUE( - 'parts', - parts, - 'must contain a non-primitive element'); - } - for (let i = 0; i < parts.length; i++) { - const value = parts[i]; - if (value !== hasLifetime) { - node = node.emplacePrimitive(value, i); - } - } - return node.get(); -}; -module.exports = { - compositeKey -}; diff --git a/node.gyp b/node.gyp index 5521837a8a4d00..88942393ff3671 100644 --- a/node.gyp +++ b/node.gyp @@ -205,7 +205,6 @@ 'lib/internal/url.js', 'lib/internal/util.js', 'lib/internal/util/comparisons.js', - 'lib/internal/util/compositekey.js', 'lib/internal/util/debuglog.js', 'lib/internal/util/inspect.js', 'lib/internal/util/inspector.js', diff --git a/test/fixtures/policy/dependencies/dependencies-scopes-policy.json b/test/fixtures/policy/dependencies/dependencies-scopes-policy.json new file mode 100644 index 00000000000000..2af78b0fa945bf --- /dev/null +++ b/test/fixtures/policy/dependencies/dependencies-scopes-policy.json @@ -0,0 +1,8 @@ +{ + "scopes": { + "../": { + "integrity": true, + "dependencies": true + } + } +} diff --git a/test/fixtures/policy/main.mjs b/test/fixtures/policy/main.mjs new file mode 100644 index 00000000000000..feac7e3b8c946c --- /dev/null +++ b/test/fixtures/policy/main.mjs @@ -0,0 +1,2 @@ +'use strict'; +export default 'main.mjs'; diff --git a/test/message/eval_messages.out b/test/message/eval_messages.out index 64743d4ae67acf..4ba0d35974fc21 100644 --- a/test/message/eval_messages.out +++ b/test/message/eval_messages.out @@ -6,8 +6,8 @@ SyntaxError: Strict mode code may not include a with statement at new Script (vm.js:*:*) at createScript (vm.js:*:*) at Object.runInThisContext (vm.js:*:*) - at Object. ([eval]-wrapper:*:*) - at Module._compile (internal/modules/cjs/loader.js:*:*) + at internal/process/execution.js:*:* + at [eval]-wrapper:*:* at evalScript (internal/process/execution.js:*:*) at internal/main/eval_string.js:*:* 42 @@ -20,8 +20,8 @@ Error: hello at [eval]:1:7 at Script.runInThisContext (vm.js:*:*) at Object.runInThisContext (vm.js:*:*) - at Object. ([eval]-wrapper:*:*) - at Module._compile (internal/modules/cjs/loader.js:*:*) + at internal/process/execution.js:*:* + at [eval]-wrapper:*:* at evalScript (internal/process/execution.js:*:*) at internal/main/eval_string.js:*:* @@ -33,8 +33,8 @@ Error: hello at [eval]:1:7 at Script.runInThisContext (vm.js:*:*) at Object.runInThisContext (vm.js:*:*) - at Object. ([eval]-wrapper:*:*) - at Module._compile (internal/modules/cjs/loader.js:*:*) + at internal/process/execution.js:*:* + at [eval]-wrapper:*:* at evalScript (internal/process/execution.js:*:*) at internal/main/eval_string.js:*:* 100 @@ -46,8 +46,8 @@ ReferenceError: y is not defined at [eval]:1:16 at Script.runInThisContext (vm.js:*:*) at Object.runInThisContext (vm.js:*:*) - at Object. ([eval]-wrapper:*:*) - at Module._compile (internal/modules/cjs/loader.js:*:*) + at internal/process/execution.js:*:* + at [eval]-wrapper:*:* at evalScript (internal/process/execution.js:*:*) at internal/main/eval_string.js:*:* diff --git a/test/message/stdin_messages.out b/test/message/stdin_messages.out index 3c71c5683b7d94..3570047012c219 100644 --- a/test/message/stdin_messages.out +++ b/test/message/stdin_messages.out @@ -7,8 +7,8 @@ SyntaxError: Strict mode code may not include a with statement at new Script (vm.js:*) at createScript (vm.js:*) at Object.runInThisContext (vm.js:*) - at Object. ([stdin]-wrapper:*:*) - at Module._compile (internal/modules/cjs/loader.js:*:*) + at internal/process/execution.js:*:* + at [stdin]-wrapper:*:* at evalScript (internal/process/execution.js:*:*) at internal/main/eval_stdin.js:*:* at Socket. (internal/process/execution.js:*:*) @@ -24,8 +24,8 @@ Error: hello at [stdin]:1:7 at Script.runInThisContext (vm.js:*) at Object.runInThisContext (vm.js:*) - at Object. ([stdin]-wrapper:*:*) - at Module._compile (internal/modules/cjs/loader.js:*:*) + at internal/process/execution.js:*:* + at [stdin]-wrapper:*:* at evalScript (internal/process/execution.js:*:*) at internal/main/eval_stdin.js:*:* at Socket. (internal/process/execution.js:*:*) @@ -39,8 +39,8 @@ Error: hello at [stdin]:1:* at Script.runInThisContext (vm.js:*) at Object.runInThisContext (vm.js:*) - at Object. ([stdin]-wrapper:*:*) - at Module._compile (internal/modules/cjs/loader.js:*:*) + at internal/process/execution.js:*:* + at [stdin]-wrapper:*:* at evalScript (internal/process/execution.js:*:*) at internal/main/eval_stdin.js:*:* at Socket. (internal/process/execution.js:*:*) @@ -55,8 +55,8 @@ ReferenceError: y is not defined at [stdin]:1:16 at Script.runInThisContext (vm.js:*) at Object.runInThisContext (vm.js:*) - at Object. ([stdin]-wrapper:*:*) - at Module._compile (internal/modules/cjs/loader.js:*:*) + at internal/process/execution.js:*:* + at [stdin]-wrapper:*:* at evalScript (internal/process/execution.js:*:*) at internal/main/eval_stdin.js:*:* at Socket. (internal/process/execution.js:*:*) diff --git a/test/parallel/test-policy-dependency-conditions.js b/test/parallel/test-policy-dependency-conditions.js index af865538ce7ebd..c9db95929daeec 100644 --- a/test/parallel/test-policy-dependency-conditions.js +++ b/test/parallel/test-policy-dependency-conditions.js @@ -84,7 +84,7 @@ for (const totalDepth of [1, 2, 3]) { } } }); - const redirector = manifest.getRedirector('test:_'); + const redirector = manifest.getDependencyMapper('test:_'); for (const { target, conditions } of order) { const result = redirector.resolve('_', conditions).href; if (result !== target) { diff --git a/test/parallel/test-policy-scopes-dependencies.js b/test/parallel/test-policy-scopes-dependencies.js new file mode 100644 index 00000000000000..202e6459a6543e --- /dev/null +++ b/test/parallel/test-policy-scopes-dependencies.js @@ -0,0 +1,306 @@ +'use strict'; +// Flags: --expose-internals + +const common = require('../common'); + +if (!common.hasCrypto) common.skip('missing crypto'); + +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: { + '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 diff --git a/test/parallel/test-policy-scopes-integrity.js b/test/parallel/test-policy-scopes-integrity.js new file mode 100644 index 00000000000000..b506c92c41f625 --- /dev/null +++ b/test/parallel/test-policy-scopes-integrity.js @@ -0,0 +1,282 @@ +'use strict'; +// Flags: --expose-internals + +const common = require('../common'); + +if (!common.hasCrypto) common.skip('missing crypto'); + +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 diff --git a/test/parallel/test-policy-scopes.js b/test/parallel/test-policy-scopes.js new file mode 100644 index 00000000000000..b1ba160bdde3f1 --- /dev/null +++ b/test/parallel/test-policy-scopes.js @@ -0,0 +1,25 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +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); +} diff --git a/test/sequential/test-inspector-async-hook-setup-at-inspect-brk.js b/test/sequential/test-inspector-async-hook-setup-at-inspect-brk.js index f4f45a1aa8f4ce..053108c415980f 100644 --- a/test/sequential/test-inspector-async-hook-setup-at-inspect-brk.js +++ b/test/sequential/test-inspector-async-hook-setup-at-inspect-brk.js @@ -24,7 +24,7 @@ async function checkAsyncStackTrace(session) { `${Object.keys(paused.params)} contains "asyncStackTrace" property`); assert(paused.params.asyncStackTrace.description, 'Timeout'); assert(paused.params.asyncStackTrace.callFrames - .some((frame) => frame.functionName === 'Module._compile')); + .some((frame) => frame.url === 'internal/process/execution.js')); } async function runTests() { diff --git a/test/sequential/test-inspector-async-stack-traces-set-interval.js b/test/sequential/test-inspector-async-stack-traces-set-interval.js index 97b2917b70092c..8f884614cc7d95 100644 --- a/test/sequential/test-inspector-async-stack-traces-set-interval.js +++ b/test/sequential/test-inspector-async-stack-traces-set-interval.js @@ -20,7 +20,7 @@ async function checkAsyncStackTrace(session) { `${Object.keys(paused.params)} contains "asyncStackTrace" property`); assert(paused.params.asyncStackTrace.description, 'Timeout'); assert(paused.params.asyncStackTrace.callFrames - .some((frame) => frame.functionName === 'Module._compile')); + .some((frame) => frame.url === 'internal/process/execution.js')); } async function runTests() {