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 <guybedford@gmail.com> Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
parent
7b6e9aedaf
commit
9d5fbeb55f
@ -90,6 +90,13 @@ added: v8.5.0
|
|||||||
|
|
||||||
Enable experimental ES module support and caching modules.
|
Enable experimental ES module support and caching modules.
|
||||||
|
|
||||||
|
### `--experimental-policy`
|
||||||
|
<!-- YAML
|
||||||
|
added: TODO
|
||||||
|
-->
|
||||||
|
|
||||||
|
Use the specified file as a security policy.
|
||||||
|
|
||||||
### `--experimental-repl-await`
|
### `--experimental-repl-await`
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v10.0.0
|
added: v10.0.0
|
||||||
|
@ -1380,6 +1380,39 @@ An attempt was made to open an IPC communication channel with a synchronously
|
|||||||
forked Node.js process. See the documentation for the [`child_process`][] module
|
forked Node.js process. See the documentation for the [`child_process`][] module
|
||||||
for more information.
|
for more information.
|
||||||
|
|
||||||
|
<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_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_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>
|
<a id="ERR_MEMORY_ALLOCATION_FAILED"></a>
|
||||||
### ERR_MEMORY_ALLOCATION_FAILED
|
### ERR_MEMORY_ALLOCATION_FAILED
|
||||||
|
|
||||||
@ -1590,6 +1623,13 @@ An attempt was made to operate on an already closed socket.
|
|||||||
|
|
||||||
A call was made and the UDP subsystem was not running.
|
A call was made and the UDP subsystem was not running.
|
||||||
|
|
||||||
|
<a id="ERR_SRI_PARSE"></a>
|
||||||
|
### ERR_SRI_PARSE
|
||||||
|
|
||||||
|
A string was provided for a Subresource Integrity check, but was unable to be
|
||||||
|
parsed. Check the format of integrity attributes by looking at the
|
||||||
|
[Subresource Integrity specification][].
|
||||||
|
|
||||||
<a id="ERR_STREAM_CANNOT_PIPE"></a>
|
<a id="ERR_STREAM_CANNOT_PIPE"></a>
|
||||||
### ERR_STREAM_CANNOT_PIPE
|
### ERR_STREAM_CANNOT_PIPE
|
||||||
|
|
||||||
@ -2229,7 +2269,9 @@ such as `process.stdout.on('data')`.
|
|||||||
[domains]: domain.html
|
[domains]: domain.html
|
||||||
[event emitter-based]: events.html#events_class_eventemitter
|
[event emitter-based]: events.html#events_class_eventemitter
|
||||||
[file descriptors]: https://en.wikipedia.org/wiki/File_descriptor
|
[file descriptors]: https://en.wikipedia.org/wiki/File_descriptor
|
||||||
|
[policy]: policy.html
|
||||||
[stream-based]: stream.html
|
[stream-based]: stream.html
|
||||||
[syscall]: http://man7.org/linux/man-pages/man2/syscalls.2.html
|
[syscall]: http://man7.org/linux/man-pages/man2/syscalls.2.html
|
||||||
|
[Subresource Integrity specification]: https://www.w3.org/TR/SRI/#the-integrity-attribute
|
||||||
[try-catch]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch
|
[try-catch]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch
|
||||||
[vm]: vm.html
|
[vm]: vm.html
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
* [OS](os.html)
|
* [OS](os.html)
|
||||||
* [Path](path.html)
|
* [Path](path.html)
|
||||||
* [Performance Hooks](perf_hooks.html)
|
* [Performance Hooks](perf_hooks.html)
|
||||||
|
* [Policies](policy.html)
|
||||||
* [Process](process.html)
|
* [Process](process.html)
|
||||||
* [Punycode](punycode.html)
|
* [Punycode](punycode.html)
|
||||||
* [Query Strings](querystring.html)
|
* [Query Strings](querystring.html)
|
||||||
|
104
doc/api/policy.md
Normal file
104
doc/api/policy.md
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# Policies
|
||||||
|
|
||||||
|
<!--introduced_in=TODO-->
|
||||||
|
<!-- type=misc -->
|
||||||
|
|
||||||
|
> Stability: 1 - Experimental
|
||||||
|
|
||||||
|
<!-- name=policy -->
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
<!-- type=misc -->
|
||||||
|
|
||||||
|
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
|
@ -86,6 +86,9 @@ Requires Node.js to be built with
|
|||||||
.It Fl -experimental-modules
|
.It Fl -experimental-modules
|
||||||
Enable experimental ES module support and caching 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
|
.It Fl -experimental-repl-await
|
||||||
Enable experimental top-level
|
Enable experimental top-level
|
||||||
.Sy await
|
.Sy await
|
||||||
|
@ -175,6 +175,28 @@ function startup() {
|
|||||||
mainThreadSetup.setupChildProcessIpcChannel();
|
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;
|
const browserGlobals = !process._noBrowserGlobals;
|
||||||
if (browserGlobals) {
|
if (browserGlobals) {
|
||||||
setupGlobalTimeouts();
|
setupGlobalTimeouts();
|
||||||
|
@ -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_DISCONNECTED', 'IPC channel is already disconnected', Error);
|
||||||
E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe', 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_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_METHOD_NOT_IMPLEMENTED', 'The %s method is not implemented', Error);
|
||||||
E('ERR_MISSING_ARGS',
|
E('ERR_MISSING_ARGS',
|
||||||
(...args) => {
|
(...args) => {
|
||||||
@ -889,6 +911,9 @@ E('ERR_SOCKET_BUFFER_SIZE',
|
|||||||
E('ERR_SOCKET_CANNOT_SEND', 'Unable to send data', Error);
|
E('ERR_SOCKET_CANNOT_SEND', 'Unable to send data', Error);
|
||||||
E('ERR_SOCKET_CLOSED', 'Socket is closed', Error);
|
E('ERR_SOCKET_CLOSED', 'Socket is closed', Error);
|
||||||
E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running', 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_CANNOT_PIPE', 'Cannot pipe, not readable', Error);
|
||||||
E('ERR_STREAM_DESTROYED', 'Cannot call %s after a stream was destroyed', 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);
|
E('ERR_STREAM_NULL_VALUES', 'May not write null values to stream', TypeError);
|
||||||
|
@ -22,8 +22,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { NativeModule } = require('internal/bootstrap/loaders');
|
const { NativeModule } = require('internal/bootstrap/loaders');
|
||||||
const util = require('util');
|
|
||||||
const { pathToFileURL } = require('internal/url');
|
const { pathToFileURL } = require('internal/url');
|
||||||
|
const util = require('util');
|
||||||
const vm = require('vm');
|
const vm = require('vm');
|
||||||
const assert = require('assert').ok;
|
const assert = require('assert').ok;
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@ -45,6 +45,9 @@ const { getOptionValue } = require('internal/options');
|
|||||||
const preserveSymlinks = getOptionValue('--preserve-symlinks');
|
const preserveSymlinks = getOptionValue('--preserve-symlinks');
|
||||||
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
|
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
|
||||||
const experimentalModules = getOptionValue('--experimental-modules');
|
const experimentalModules = getOptionValue('--experimental-modules');
|
||||||
|
const manifest = getOptionValue('[has_experimental_policy]') ?
|
||||||
|
require('internal/process/policy').manifest :
|
||||||
|
null;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ERR_INVALID_ARG_VALUE,
|
ERR_INVALID_ARG_VALUE,
|
||||||
@ -168,6 +171,11 @@ function readPackage(requestPath) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (manifest) {
|
||||||
|
const jsonURL = pathToFileURL(jsonPath);
|
||||||
|
manifest.assertIntegrity(jsonURL, json);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return packageMainCache[requestPath] = JSON.parse(json).main;
|
return packageMainCache[requestPath] = JSON.parse(json).main;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -676,6 +684,10 @@ function normalizeReferrerURL(referrer) {
|
|||||||
// the file.
|
// the file.
|
||||||
// Returns exception, if any.
|
// Returns exception, if any.
|
||||||
Module.prototype._compile = function(content, filename) {
|
Module.prototype._compile = function(content, filename) {
|
||||||
|
if (manifest) {
|
||||||
|
const moduleURL = pathToFileURL(filename);
|
||||||
|
manifest.assertIntegrity(moduleURL, content);
|
||||||
|
}
|
||||||
|
|
||||||
content = stripShebang(content);
|
content = stripShebang(content);
|
||||||
|
|
||||||
@ -715,11 +727,14 @@ Module.prototype._compile = function(content, filename) {
|
|||||||
var depth = requireDepth;
|
var depth = requireDepth;
|
||||||
if (depth === 0) stat.cache = new Map();
|
if (depth === 0) stat.cache = new Map();
|
||||||
var result;
|
var result;
|
||||||
|
var exports = this.exports;
|
||||||
|
var thisValue = exports;
|
||||||
|
var module = this;
|
||||||
if (inspectorWrapper) {
|
if (inspectorWrapper) {
|
||||||
result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
|
result = inspectorWrapper(compiledWrapper, thisValue, exports,
|
||||||
require, this, filename, dirname);
|
require, module, filename, dirname);
|
||||||
} else {
|
} else {
|
||||||
result = compiledWrapper.call(this.exports, this.exports, require, this,
|
result = compiledWrapper.call(thisValue, exports, require, module,
|
||||||
filename, dirname);
|
filename, dirname);
|
||||||
}
|
}
|
||||||
if (depth === 0) stat.cache = null;
|
if (depth === 0) stat.cache = null;
|
||||||
@ -736,7 +751,13 @@ Module._extensions['.js'] = function(module, filename) {
|
|||||||
|
|
||||||
// Native extension for .json
|
// Native extension for .json
|
||||||
Module._extensions['.json'] = function(module, filename) {
|
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 {
|
try {
|
||||||
module.exports = JSON.parse(stripBOM(content));
|
module.exports = JSON.parse(stripBOM(content));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -748,6 +769,12 @@ Module._extensions['.json'] = function(module, filename) {
|
|||||||
|
|
||||||
// Native extension for .node
|
// Native extension for .node
|
||||||
Module._extensions['.node'] = function(module, filename) {
|
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));
|
return process.dlopen(module, path.toNamespacedPath(filename));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
130
lib/internal/policy/manifest.js
Normal file
130
lib/internal/policy/manifest.js
Normal file
@ -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 });
|
68
lib/internal/policy/sri.js
Normal file
68
lib/internal/policy/sri.js
Normal file
@ -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,
|
||||||
|
};
|
33
lib/internal/process/policy.js
Normal file
33
lib/internal/process/policy.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
});
|
@ -20,5 +20,6 @@ const makeSafe = (unsafe, safe) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
exports.SafeMap = makeSafe(Map, class SafeMap extends Map {});
|
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.SafeSet = makeSafe(Set, class SafeSet extends Set {});
|
||||||
exports.SafePromise = makeSafe(Promise, class SafePromise extends Promise {});
|
exports.SafePromise = makeSafe(Promise, class SafePromise extends Promise {});
|
||||||
|
3
node.gyp
3
node.gyp
@ -142,6 +142,8 @@
|
|||||||
'lib/internal/safe_globals.js',
|
'lib/internal/safe_globals.js',
|
||||||
'lib/internal/net.js',
|
'lib/internal/net.js',
|
||||||
'lib/internal/options.js',
|
'lib/internal/options.js',
|
||||||
|
'lib/internal/policy/sri.js',
|
||||||
|
'lib/internal/policy/manifest.js',
|
||||||
'lib/internal/print_help.js',
|
'lib/internal/print_help.js',
|
||||||
'lib/internal/priority_queue.js',
|
'lib/internal/priority_queue.js',
|
||||||
'lib/internal/process/esm_loader.js',
|
'lib/internal/process/esm_loader.js',
|
||||||
@ -149,6 +151,7 @@
|
|||||||
'lib/internal/process/main_thread_only.js',
|
'lib/internal/process/main_thread_only.js',
|
||||||
'lib/internal/process/next_tick.js',
|
'lib/internal/process/next_tick.js',
|
||||||
'lib/internal/process/per_thread.js',
|
'lib/internal/process/per_thread.js',
|
||||||
|
'lib/internal/process/policy.js',
|
||||||
'lib/internal/process/promises.js',
|
'lib/internal/process/promises.js',
|
||||||
'lib/internal/process/stdio.js',
|
'lib/internal/process/stdio.js',
|
||||||
'lib/internal/process/warning.js',
|
'lib/internal/process/warning.js',
|
||||||
|
@ -101,6 +101,15 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
|
|||||||
"experimental ES Module support and caching modules",
|
"experimental ES Module support and caching modules",
|
||||||
&EnvironmentOptions::experimental_modules,
|
&EnvironmentOptions::experimental_modules,
|
||||||
kAllowedInEnvironment);
|
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",
|
AddOption("--experimental-repl-await",
|
||||||
"experimental await keyword support in REPL",
|
"experimental await keyword support in REPL",
|
||||||
&EnvironmentOptions::experimental_repl_await,
|
&EnvironmentOptions::experimental_repl_await,
|
||||||
|
@ -94,6 +94,8 @@ class EnvironmentOptions : public Options {
|
|||||||
public:
|
public:
|
||||||
bool abort_on_uncaught_exception = false;
|
bool abort_on_uncaught_exception = false;
|
||||||
bool experimental_modules = false;
|
bool experimental_modules = false;
|
||||||
|
std::string experimental_policy;
|
||||||
|
bool has_experimental_policy;
|
||||||
bool experimental_repl_await = false;
|
bool experimental_repl_await = false;
|
||||||
bool experimental_vm_modules = false;
|
bool experimental_vm_modules = false;
|
||||||
bool expose_internals = false;
|
bool expose_internals = false;
|
||||||
|
297
test/parallel/test-policy-integrity.js
Normal file
297
test/parallel/test-policy-integrity.js
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user