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.
|
||||
|
||||
### `--experimental-policy`
|
||||
<!-- YAML
|
||||
added: TODO
|
||||
-->
|
||||
|
||||
Use the specified file as a security policy.
|
||||
|
||||
### `--experimental-repl-await`
|
||||
<!-- YAML
|
||||
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
|
||||
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>
|
||||
### 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 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>
|
||||
### ERR_STREAM_CANNOT_PIPE
|
||||
|
||||
@ -2229,7 +2269,9 @@ such as `process.stdout.on('data')`.
|
||||
[domains]: domain.html
|
||||
[event emitter-based]: events.html#events_class_eventemitter
|
||||
[file descriptors]: https://en.wikipedia.org/wiki/File_descriptor
|
||||
[policy]: policy.html
|
||||
[stream-based]: stream.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
|
||||
[vm]: vm.html
|
||||
|
@ -39,6 +39,7 @@
|
||||
* [OS](os.html)
|
||||
* [Path](path.html)
|
||||
* [Performance Hooks](perf_hooks.html)
|
||||
* [Policies](policy.html)
|
||||
* [Process](process.html)
|
||||
* [Punycode](punycode.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
|
||||
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
|
||||
|
@ -175,6 +175,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();
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
@ -168,6 +171,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) {
|
||||
@ -676,6 +684,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);
|
||||
|
||||
@ -715,11 +727,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;
|
||||
@ -736,7 +751,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) {
|
||||
@ -748,6 +769,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));
|
||||
};
|
||||
|
||||
|
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.SafeWeakMap = makeSafe(WeakMap, class SafeWeakMap extends WeakMap {});
|
||||
exports.SafeSet = makeSafe(Set, class SafeSet extends Set {});
|
||||
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/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',
|
||||
@ -149,6 +151,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',
|
||||
|
@ -101,6 +101,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,
|
||||
|
@ -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;
|
||||
|
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