summaryrefslogtreecommitdiff
path: root/lib/internal
diff options
context:
space:
mode:
authorBradley Farias <bradley.meck@gmail.com>2018-09-13 14:27:12 -0500
committerBradley Farias <bradley.meck@gmail.com>2019-01-17 09:43:42 -0600
commit9d5fbeb55fb1927928237e09475d39346d9c3ad9 (patch)
treeca2f567ff647c9a1706f39e93e54caa03cd98c1d /lib/internal
parent7b6e9aedaf8c9aa219ff759bed6b1680910eefe0 (diff)
downloadandroid-node-v8-9d5fbeb55fb1927928237e09475d39346d9c3ad9.tar.gz
android-node-v8-9d5fbeb55fb1927928237e09475d39346d9c3ad9.tar.bz2
android-node-v8-9d5fbeb55fb1927928237e09475d39346d9c3ad9.zip
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>
Diffstat (limited to 'lib/internal')
-rw-r--r--lib/internal/bootstrap/node.js22
-rw-r--r--lib/internal/errors.js25
-rw-r--r--lib/internal/modules/cjs/loader.js37
-rw-r--r--lib/internal/policy/manifest.js130
-rw-r--r--lib/internal/policy/sri.js68
-rw-r--r--lib/internal/process/policy.js33
-rw-r--r--lib/internal/safe_globals.js1
7 files changed, 311 insertions, 5 deletions
diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js
index 26585f5df9..21f642e4d1 100644
--- a/lib/internal/bootstrap/node.js
+++ b/lib/internal/bootstrap/node.js
@@ -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();
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 7002776300..45701c8252 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 f4551b7d47..1dcf5ef6ac 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,
@@ -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));
};
diff --git a/lib/internal/policy/manifest.js b/lib/internal/policy/manifest.js
new file mode 100644
index 0000000000..272abf2457
--- /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 0000000000..fff4e066b1
--- /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 0000000000..f5ca4eeb07
--- /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 31de4137f0..109409d535 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 {});