From da8c526888bf26b2a9efad541abdb28b12dc7331 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Thu, 13 Sep 2018 14:27:12 -0500 Subject: [PATCH] policy: manifest with subresource integrity checks This enables code loaded via the module system to be checked for integrity to ensure the code loaded matches expectations. PR-URL: https://github.com/nodejs/node/pull/23834 Reviewed-By: Guy Bedford Reviewed-By: Vladimir de Turckheim Reviewed-By: Matteo Collina --- doc/api/cli.md | 7 + doc/api/errors.md | 42 ++++ doc/api/index.md | 1 + doc/api/policy.md | 104 +++++++++ doc/node.1 | 3 + lib/internal/bootstrap/node.js | 22 ++ lib/internal/errors.js | 25 +++ lib/internal/modules/cjs/loader.js | 37 ++- lib/internal/policy/manifest.js | 130 +++++++++++ lib/internal/policy/sri.js | 68 ++++++ lib/internal/process/policy.js | 33 +++ lib/internal/safe_globals.js | 1 + node.gyp | 3 + src/node_options.cc | 9 + src/node_options.h | 2 + test/parallel/test-policy-integrity.js | 297 +++++++++++++++++++++++++ 16 files changed, 779 insertions(+), 5 deletions(-) create mode 100644 doc/api/policy.md create mode 100644 lib/internal/policy/manifest.js create mode 100644 lib/internal/policy/sri.js create mode 100644 lib/internal/process/policy.js create mode 100644 test/parallel/test-policy-integrity.js diff --git a/doc/api/cli.md b/doc/api/cli.md index c2c8a59ced2033..ccc86dedc1cf50 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -90,6 +90,13 @@ added: v8.5.0 Enable experimental ES module support and caching modules. +### `--experimental-policy` + + +Use the specified file as a security policy. + ### `--experimental-repl-await` + + +> Stability: 1 - Experimental + + + +Node.js contains experimental support for creating policies on loading code. + +Policies are a security feature intended to allow guarantees +about what code Node.js is able to load. The use of policies assumes +safe practices for the policy files such as ensuring that policy +files cannot be overwritten by the Node.js application by using +file permissions. + +A best practice would be to ensure that the policy manifest is read only for +the running Node.js application, and that the file cannot be changed +by the running Node.js application in any way. A typical setup would be to +create the policy file as a different user id than the one running Node.js +and granting read permissions to the user id running Node.js. + +## Enabling + + + +The `--experimental-policy` flag can be used to enable features for policies +when loading modules. + +Once this has been set, all modules must conform to a policy manifest file +passed to the flag: + +```sh +node --experimental-policy=policy.json app.js +``` + +The policy manifest will be used to enforce constraints on code loaded by +Node.js. + +## Features + +### Error Behavior + +When a policy check fails, Node.js by default will throw an error. +It is possible to change the error behavior to one of a few possibilities +by defining an "onerror" field in a policy manifest. The following values are +available to change the behavior: + +* `"exit"` - will exit the process immediately. + No cleanup code will be allowed to run. +* `"log"` - will log the error at the site of the failure. +* `"throw"` (default) - will throw a JS error at the site of the failure. + +```json +{ + "onerror": "log", + "resources": { + "./app/checked.js": { + "integrity": "sha384-SggXRQHwCG8g+DktYYzxkXRIkTiEYWBHqev0xnpCxYlqMBufKZHAHQM3/boDaI/0" + } + } +} +``` + +### Integrity Checks + +Policy files must use integrity checks with Subresource Integrity strings +compatible with the browser +[integrity attribute](https://www.w3.org/TR/SRI/#the-integrity-attribute) +associated with absolute URLs. + +When using `require()` all resources involved in loading are checked for +integrity if a policy manifest has been specified. If a resource does not match +the integrity listed in the manifest, an error will be thrown. + +An example policy file that would allow loading a file `checked.js`: + +```json +{ + "resources": { + "./app/checked.js": { + "integrity": "sha384-SggXRQHwCG8g+DktYYzxkXRIkTiEYWBHqev0xnpCxYlqMBufKZHAHQM3/boDaI/0" + } + } +} +``` + +Each resource listed in the policy manifest can be of one the following +formats to determine its location: + +1. A [relative url string][] to a resource from the manifest such as `./resource.js`, `../resource.js`, or `/resource.js`. +2. A complete url string to a resource such as `file:///resource.js`. + +When loading resources the entire URL must match including search parameters +and hash fragment. `./a.js?b` will not be used when attempting to load +`./a.js` and vice versa. + +In order to generate integrity strings, a script such as +`printf "sha384-$(cat checked.js | openssl dgst -sha384 -binary | base64)"` +can be used. + + +[relative url string]: https://url.spec.whatwg.org/#relative-url-with-fragment-string diff --git a/doc/node.1 b/doc/node.1 index 09c91f3a41adb1..c5ec38bb34c35d 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -86,6 +86,9 @@ Requires Node.js to be built with .It Fl -experimental-modules Enable experimental ES module support and caching modules. . +.It Fl -experimental-policy +Use the specified file as a security policy. +. .It Fl -experimental-repl-await Enable experimental top-level .Sy await diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index 1ad2dce622215c..d8313210fd5248 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -171,6 +171,28 @@ function startup() { mainThreadSetup.setupChildProcessIpcChannel(); } + // TODO(joyeecheung): move this down further to get better snapshotting + if (getOptionValue('[has_experimental_policy]')) { + process.emitWarning('Policies are experimental.', + 'ExperimentalWarning'); + const experimentalPolicy = getOptionValue('--experimental-policy'); + const { pathToFileURL, URL } = NativeModule.require('url'); + // URL here as it is slightly different parsing + // no bare specifiers for now + let manifestURL; + if (NativeModule.require('path').isAbsolute(experimentalPolicy)) { + manifestURL = new URL(`file:///${experimentalPolicy}`); + } else { + const cwdURL = pathToFileURL(process.cwd()); + cwdURL.pathname += '/'; + manifestURL = new URL(experimentalPolicy, cwdURL); + } + const fs = NativeModule.require('fs'); + const src = fs.readFileSync(manifestURL, 'utf8'); + NativeModule.require('internal/process/policy') + .setup(src, manifestURL.href); + } + const browserGlobals = !process._noBrowserGlobals; if (browserGlobals) { setupGlobalTimeouts(); diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 8cc9bbcd2d74db..12b01ffb9b64f3 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -818,6 +818,28 @@ E('ERR_IPC_CHANNEL_CLOSED', 'Channel closed', Error); E('ERR_IPC_DISCONNECTED', 'IPC channel is already disconnected', Error); E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe', Error); E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks', 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 = [...realIntegrities.entries()].map(([alg, dgs]) => { + return `${alg}-${dgs}`; + }).join(' '); + msg += ` Integrities found are: ${sri}`; + } else { + msg += ' The resource was not found in the policy.'; + } + return msg; + }, Error); +E('ERR_MANIFEST_INTEGRITY_MISMATCH', + 'Manifest resource %s has multiple entries but integrity lists do not match', + SyntaxError); +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) => { @@ -889,6 +911,9 @@ E('ERR_SOCKET_BUFFER_SIZE', E('ERR_SOCKET_CANNOT_SEND', 'Unable to send data', Error); E('ERR_SOCKET_CLOSED', 'Socket is closed', Error); E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running', Error); +E('ERR_SRI_PARSE', + 'Subresource Integrity string %s had an unexpected at %d', + SyntaxError); E('ERR_STREAM_CANNOT_PIPE', 'Cannot pipe, not readable', Error); E('ERR_STREAM_DESTROYED', 'Cannot call %s after a stream was destroyed', Error); E('ERR_STREAM_NULL_VALUES', 'May not write null values to stream', TypeError); diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 89e4c56e2e790c..bc1ddbcf241816 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -22,8 +22,8 @@ 'use strict'; const { NativeModule } = require('internal/bootstrap/loaders'); -const util = require('util'); const { pathToFileURL } = require('internal/url'); +const util = require('util'); const vm = require('vm'); const assert = require('assert').ok; const fs = require('fs'); @@ -45,6 +45,9 @@ const { getOptionValue } = require('internal/options'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); const experimentalModules = getOptionValue('--experimental-modules'); +const manifest = getOptionValue('[has_experimental_policy]') ? + require('internal/process/policy').manifest : + null; const { ERR_INVALID_ARG_VALUE, @@ -164,6 +167,11 @@ function readPackage(requestPath) { return false; } + if (manifest) { + const jsonURL = pathToFileURL(jsonPath); + manifest.assertIntegrity(jsonURL, json); + } + try { return packageMainCache[requestPath] = JSON.parse(json).main; } catch (e) { @@ -675,6 +683,10 @@ function normalizeReferrerURL(referrer) { // the file. // Returns exception, if any. Module.prototype._compile = function(content, filename) { + if (manifest) { + const moduleURL = pathToFileURL(filename); + manifest.assertIntegrity(moduleURL, content); + } content = stripShebang(content); @@ -714,11 +726,14 @@ Module.prototype._compile = function(content, filename) { var depth = requireDepth; if (depth === 0) stat.cache = new Map(); var result; + var exports = this.exports; + var thisValue = exports; + var module = this; if (inspectorWrapper) { - result = inspectorWrapper(compiledWrapper, this.exports, this.exports, - require, this, filename, dirname); + result = inspectorWrapper(compiledWrapper, thisValue, exports, + require, module, filename, dirname); } else { - result = compiledWrapper.call(this.exports, this.exports, require, this, + result = compiledWrapper.call(thisValue, exports, require, module, filename, dirname); } if (depth === 0) stat.cache = null; @@ -735,7 +750,13 @@ Module._extensions['.js'] = function(module, filename) { // Native extension for .json Module._extensions['.json'] = function(module, filename) { - var content = fs.readFileSync(filename, 'utf8'); + const content = fs.readFileSync(filename, 'utf8'); + + if (manifest) { + const moduleURL = pathToFileURL(filename); + manifest.assertIntegrity(moduleURL, content); + } + try { module.exports = JSON.parse(stripBOM(content)); } catch (err) { @@ -747,6 +768,12 @@ Module._extensions['.json'] = function(module, filename) { // Native extension for .node Module._extensions['.node'] = function(module, filename) { + 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)); }; diff --git a/lib/internal/policy/manifest.js b/lib/internal/policy/manifest.js new file mode 100644 index 00000000000000..272abf2457ddc3 --- /dev/null +++ b/lib/internal/policy/manifest.js @@ -0,0 +1,130 @@ +'use strict'; +const { + ERR_MANIFEST_ASSERT_INTEGRITY, + ERR_MANIFEST_INTEGRITY_MISMATCH, + ERR_MANIFEST_UNKNOWN_ONERROR, +} = require('internal/errors').codes; +const debug = require('util').debuglog('policy'); +const SRI = require('internal/policy/sri'); +const { SafeWeakMap } = require('internal/safe_globals'); +const crypto = require('crypto'); +const { Buffer } = require('buffer'); +const { URL } = require('url'); +const { createHash, timingSafeEqual } = crypto; +const HashUpdate = Function.call.bind(crypto.Hash.prototype.update); +const HashDigest = Function.call.bind(crypto.Hash.prototype.digest); +const BufferEquals = Function.call.bind(Buffer.prototype.equals); +const BufferToString = Function.call.bind(Buffer.prototype.toString); +const RegExpTest = Function.call.bind(RegExp.prototype.test); +const { entries } = Object; +const kIntegrities = new SafeWeakMap(); +const kReactions = new SafeWeakMap(); +const kRelativeURLStringPattern = /^\.{0,2}\//; +const { shouldAbortOnUncaughtException } = internalBinding('config'); +const { abort, exit, _rawDebug } = process; +function REACTION_THROW(error) { + throw error; +} +function REACTION_EXIT(error) { + REACTION_LOG(error); + if (shouldAbortOnUncaughtException) { + abort(); + } + exit(1); +} +function REACTION_LOG(error) { + _rawDebug(error.stack); +} +class Manifest { + constructor(obj, manifestURL) { + const integrities = { + __proto__: null, + }; + const reactions = { + __proto__: null, + integrity: REACTION_THROW, + }; + if (obj.onerror) { + const behavior = obj.onerror; + if (behavior === 'throw') { + } else if (behavior === 'exit') { + reactions.integrity = REACTION_EXIT; + } else if (behavior === 'log') { + reactions.integrity = REACTION_LOG; + } else { + throw new ERR_MANIFEST_UNKNOWN_ONERROR(behavior); + } + } + kReactions.set(this, Object.freeze(reactions)); + const manifestEntries = entries(obj.resources); + for (var i = 0; i < manifestEntries.length; i++) { + let url = manifestEntries[i][0]; + const integrity = manifestEntries[i][1].integrity; + if (integrity != null) { + debug(`Manifest contains integrity for url ${url}`); + if (RegExpTest(kRelativeURLStringPattern, url)) { + url = new URL(url, manifestURL).href; + } + const sri = Object.freeze(SRI.parse(integrity)); + if (url in integrities) { + const old = integrities[url]; + let mismatch = false; + if (old.length !== sri.length) { + mismatch = true; + } else { + compare: + for (var sriI = 0; sriI < sri.length; sriI++) { + for (var oldI = 0; oldI < old.length; oldI++) { + if (sri[sriI].algorithm === old[oldI].algorithm && + BufferEquals(sri[sriI].value, old[oldI].value) && + sri[sriI].options === old[oldI].options) { + continue compare; + } + } + mismatch = true; + break compare; + } + } + if (mismatch) { + throw new ERR_MANIFEST_INTEGRITY_MISMATCH(url); + } + } + integrities[url] = sri; + } + } + Object.freeze(integrities); + kIntegrities.set(this, integrities); + Object.freeze(this); + } + assertIntegrity(url, content) { + debug(`Checking integrity of ${url}`); + const integrities = kIntegrities.get(this); + const realIntegrities = new Map(); + if (integrities && url in integrities) { + const integrityEntries = integrities[url]; + // Avoid clobbered Symbol.iterator + for (var 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; + } + realIntegrities.set(algorithm, BufferToString(digest, 'base64')); + } + } + const error = new ERR_MANIFEST_ASSERT_INTEGRITY(url, realIntegrities); + kReactions.get(this).integrity(error); + } +} +// Lock everything down to avoid problems even if reference is leaked somehow +Object.setPrototypeOf(Manifest, null); +Object.setPrototypeOf(Manifest.prototype, null); +Object.freeze(Manifest); +Object.freeze(Manifest.prototype); +module.exports = Object.freeze({ Manifest }); diff --git a/lib/internal/policy/sri.js b/lib/internal/policy/sri.js new file mode 100644 index 00000000000000..fff4e066b17451 --- /dev/null +++ b/lib/internal/policy/sri.js @@ -0,0 +1,68 @@ +'use strict'; +// Value of https://w3c.github.io/webappsec-subresource-integrity/#the-integrity-attribute + +// Returns [{algorithm, value (in base64 string), options,}] +const { + ERR_SRI_PARSE +} = require('internal/errors').codes; +const kWSP = '[\\x20\\x09]'; +const kVCHAR = '[\\x21-\\x7E]'; +const kHASH_ALGO = 'sha256|sha384|sha512'; +// Base64 +const kHASH_VALUE = '[A-Za-z0-9+/]+[=]{0,2}'; +const kHASH_EXPRESSION = `(${kHASH_ALGO})-(${kHASH_VALUE})`; +const kOPTION_EXPRESSION = `(${kVCHAR}*)`; +const kHASH_WITH_OPTIONS = `${kHASH_EXPRESSION}(?:[?](${kOPTION_EXPRESSION}))?`; +const kSRIPattern = new RegExp(`(${kWSP}*)(?:${kHASH_WITH_OPTIONS})`, 'g'); +const { freeze } = Object; +Object.seal(kSRIPattern); +const kAllWSP = new RegExp(`^${kWSP}*$`); +Object.seal(kAllWSP); +const RegExpExec = Function.call.bind(RegExp.prototype.exec); +const RegExpTest = Function.call.bind(RegExp.prototype.test); +const StringSlice = Function.call.bind(String.prototype.slice); +const { + Buffer: { + from: BufferFrom + } +} = require('buffer'); +const { defineProperty } = Object; +const parse = (str) => { + kSRIPattern.lastIndex = 0; + let prevIndex = 0; + let match = RegExpExec(kSRIPattern, str); + const entries = []; + while (match) { + if (match.index !== prevIndex) { + throw new ERR_SRI_PARSE(str, prevIndex); + } + if (entries.length > 0) { + if (match[1] === '') { + throw new ERR_SRI_PARSE(str, prevIndex); + } + } + // Avoid setters being fired + defineProperty(entries, entries.length, { + enumerable: true, + configurable: true, + value: freeze({ + __proto__: null, + algorithm: match[2], + value: BufferFrom(match[3], 'base64'), + options: match[4] === undefined ? null : match[4], + }) + }); + prevIndex = prevIndex + match[0].length; + match = RegExpExec(kSRIPattern, str); + } + if (prevIndex !== str.length) { + if (!RegExpTest(kAllWSP, StringSlice(str, prevIndex))) { + throw new ERR_SRI_PARSE(str, prevIndex); + } + } + return entries; +}; + +module.exports = { + parse, +}; diff --git a/lib/internal/process/policy.js b/lib/internal/process/policy.js new file mode 100644 index 00000000000000..f5ca4eeb07a3e0 --- /dev/null +++ b/lib/internal/process/policy.js @@ -0,0 +1,33 @@ +'use strict'; + +const { + ERR_MANIFEST_TDZ, +} = require('internal/errors').codes; +const { Manifest } = require('internal/policy/manifest'); +let manifest; +module.exports = Object.freeze({ + __proto__: null, + setup(src, url) { + if (src === null) { + manifest = null; + return; + } + const json = JSON.parse(src, (_, o) => { + if (o && typeof o === 'object') { + Reflect.setPrototypeOf(o, null); + Object.freeze(o); + } + return o; + }); + manifest = new Manifest(json, url); + }, + get manifest() { + if (typeof manifest === 'undefined') { + throw new ERR_MANIFEST_TDZ(); + } + return manifest; + }, + assertIntegrity(moduleURL, content) { + this.manifest.matchesIntegrity(moduleURL, content); + } +}); diff --git a/lib/internal/safe_globals.js b/lib/internal/safe_globals.js index 31de4137f0ad53..109409d535495d 100644 --- a/lib/internal/safe_globals.js +++ b/lib/internal/safe_globals.js @@ -20,5 +20,6 @@ const makeSafe = (unsafe, safe) => { }; exports.SafeMap = makeSafe(Map, class SafeMap extends Map {}); +exports.SafeWeakMap = makeSafe(WeakMap, class SafeWeakMap extends WeakMap {}); exports.SafeSet = makeSafe(Set, class SafeSet extends Set {}); exports.SafePromise = makeSafe(Promise, class SafePromise extends Promise {}); diff --git a/node.gyp b/node.gyp index 17e4b27dd6e09a..d5ca93b4b7ef90 100644 --- a/node.gyp +++ b/node.gyp @@ -141,6 +141,8 @@ 'lib/internal/safe_globals.js', 'lib/internal/net.js', 'lib/internal/options.js', + 'lib/internal/policy/sri.js', + 'lib/internal/policy/manifest.js', 'lib/internal/print_help.js', 'lib/internal/priority_queue.js', 'lib/internal/process/esm_loader.js', @@ -148,6 +150,7 @@ 'lib/internal/process/main_thread_only.js', 'lib/internal/process/next_tick.js', 'lib/internal/process/per_thread.js', + 'lib/internal/process/policy.js', 'lib/internal/process/promises.js', 'lib/internal/process/stdio.js', 'lib/internal/process/warning.js', diff --git a/src/node_options.cc b/src/node_options.cc index f5011fbe4bcb66..dcbeac97f689a8 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -102,6 +102,15 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "experimental ES Module support and caching modules", &EnvironmentOptions::experimental_modules, kAllowedInEnvironment); + AddOption("[has_experimental_policy]", + "", + &EnvironmentOptions::has_experimental_policy); + AddOption("--experimental-policy", + "use the specified file as a " + "security policy", + &EnvironmentOptions::experimental_policy, + kAllowedInEnvironment); + Implies("--experimental-policy", "[has_experimental_policy]"); AddOption("--experimental-repl-await", "experimental await keyword support in REPL", &EnvironmentOptions::experimental_repl_await, diff --git a/src/node_options.h b/src/node_options.h index aae3743306d50c..47e300e8916ddc 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -94,6 +94,8 @@ class EnvironmentOptions : public Options { public: bool abort_on_uncaught_exception = false; bool experimental_modules = false; + std::string experimental_policy; + bool has_experimental_policy; bool experimental_repl_await = false; bool experimental_vm_modules = false; bool expose_internals = false; diff --git a/test/parallel/test-policy-integrity.js b/test/parallel/test-policy-integrity.js new file mode 100644 index 00000000000000..5c1ea4fc4eed64 --- /dev/null +++ b/test/parallel/test-policy-integrity.js @@ -0,0 +1,297 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +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 policyFilepath = path.join(tmpdir.path, 'policy'); + +const packageFilepath = path.join(tmpdir.path, 'package.json'); +const packageURL = pathToFileURL(packageFilepath); +const packageBody = '{"main": "dep.js"}'; +const policyToPackageRelativeURLString = `./${ + path.relative(path.dirname(policyFilepath), packageFilepath) +}`; + +const parentFilepath = path.join(tmpdir.path, 'parent.js'); +const parentURL = pathToFileURL(parentFilepath); +const parentBody = 'require(\'./dep.js\')'; + +const depFilepath = path.join(tmpdir.path, 'dep.js'); +const depURL = pathToFileURL(depFilepath); +const depBody = ''; +const policyToDepRelativeURLString = `./${ + path.relative(path.dirname(policyFilepath), depFilepath) +}`; + +fs.writeFileSync(parentFilepath, parentBody); +fs.writeFileSync(depFilepath, depBody); + +const tmpdirURL = pathToFileURL(tmpdir.path); +if (!tmpdirURL.pathname.endsWith('/')) { + tmpdirURL.pathname += '/'; +} +function test({ + shouldFail = false, + entry, + onerror, + resources = {} +}) { + const manifest = { + onerror, + resources: {} + }; + for (const [url, { body, match }] of Object.entries(resources)) { + manifest.resources[url] = { + integrity: `sha256-${hash('sha256', match ? body : body + '\n')}` + }; + fs.writeFileSync(new URL(url, tmpdirURL.href), body); + } + fs.writeFileSync(policyFilepath, JSON.stringify(manifest, null, 2)); + const { status } = spawnSync(process.execPath, [ + '--experimental-policy', policyFilepath, entry + ]); + if (shouldFail) { + assert.notStrictEqual(status, 0); + } else { + assert.strictEqual(status, 0); + } +} + +const { status } = spawnSync(process.execPath, [ + '--experimental-policy', policyFilepath, + '--experimental-policy', policyFilepath +], { + stdio: 'pipe' +}); +assert.notStrictEqual(status, 0, 'Should not allow multiple policies'); + +test({ + shouldFail: true, + entry: parentFilepath, + resources: { + } +}); +test({ + shouldFail: false, + entry: parentFilepath, + onerror: 'log', +}); +test({ + shouldFail: true, + entry: parentFilepath, + onerror: 'exit', +}); +test({ + shouldFail: true, + entry: parentFilepath, + onerror: 'throw', +}); +test({ + shouldFail: true, + entry: parentFilepath, + onerror: 'unknown-onerror-value', +}); +test({ + shouldFail: true, + entry: path.dirname(packageFilepath), + resources: { + } +}); +test({ + shouldFail: true, + entry: path.dirname(packageFilepath), + resources: { + [depURL]: { + body: depBody, + match: true, + } + } +}); +test({ + shouldFail: false, + entry: path.dirname(packageFilepath), + onerror: 'log', + resources: { + [packageURL]: { + body: packageBody, + match: false, + }, + [depURL]: { + body: depBody, + match: true, + } + } +}); +test({ + shouldFail: true, + entry: path.dirname(packageFilepath), + resources: { + [packageURL]: { + body: packageBody, + match: false, + }, + [depURL]: { + body: depBody, + match: true, + } + } +}); +test({ + shouldFail: true, + entry: path.dirname(packageFilepath), + resources: { + [packageURL]: { + body: packageBody, + match: true, + }, + [depURL]: { + body: depBody, + match: false, + } + } +}); +test({ + shouldFail: false, + entry: path.dirname(packageFilepath), + resources: { + [packageURL]: { + body: packageBody, + match: true, + }, + [depURL]: { + body: depBody, + match: true, + } + } +}); +test({ + shouldFail: false, + entry: parentFilepath, + resources: { + [parentURL]: { + body: parentBody, + match: true, + }, + [depURL]: { + body: depBody, + match: true, + } + } +}); +test({ + shouldFail: true, + entry: parentFilepath, + resources: { + [parentURL]: { + body: parentBody, + match: false, + }, + [depURL]: { + body: depBody, + match: true, + } + } +}); +test({ + shouldFail: true, + entry: parentFilepath, + resources: { + [parentURL]: { + body: parentBody, + match: true, + }, + [depURL]: { + body: depBody, + match: false, + } + } +}); +test({ + shouldFail: true, + entry: parentFilepath, + resources: { + [parentURL]: { + body: parentBody, + match: true, + } + } +}); +test({ + shouldFail: false, + entry: depFilepath, + resources: { + [depURL]: { + body: depBody, + match: true, + } + } +}); +test({ + shouldFail: false, + entry: depFilepath, + resources: { + [policyToDepRelativeURLString]: { + body: depBody, + match: true, + } + } +}); +test({ + shouldFail: true, + entry: depFilepath, + resources: { + [policyToDepRelativeURLString]: { + body: depBody, + match: false, + } + } +}); +test({ + shouldFail: false, + entry: depFilepath, + resources: { + [policyToDepRelativeURLString]: { + body: depBody, + match: true, + }, + [depURL]: { + body: depBody, + match: true, + } + } +}); +test({ + shouldFail: true, + entry: depFilepath, + resources: { + [policyToPackageRelativeURLString]: { + body: packageBody, + match: true, + }, + [packageURL]: { + body: packageBody, + match: true, + }, + [depURL]: { + body: depBody, + match: false, + } + } +});