policy: add startup benchmark and make SRI lazier

PR-URL: https://github.com/nodejs/node/pull/29527
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Bradley Farias 2019-09-11 10:03:11 -05:00 committed by Anna Henningsen
parent 7be8dded52
commit 0ede223fa8
No known key found for this signature in database
GPG Key ID: A94130F0BFC8EBE9
4 changed files with 119 additions and 33 deletions

View File

@ -0,0 +1,51 @@
// Tests the impact on eager operations required for policies affecting
// general startup, does not test lazy operations
'use strict';
const common = require('../common.js');
const configs = {
n: [1024]
};
const options = {
flags: ['--expose-internals']
};
const bench = common.createBenchmark(main, configs, options);
function main(conf) {
const hash = (str, algo) => {
const hash = require('crypto').createHash(algo);
return hash.update(str).digest('base64');
};
const resources = Object.fromEntries(
// Simulate graph of 1k modules
Array.from({ length: 1024 }, (_, i) => {
return [`./_${i}`, {
integrity: `sha256-${hash(`// ./_${i}`, 'sha256')}`,
dependencies: Object.fromEntries(Array.from({
// Average 3 deps per 4 modules
length: Math.floor((i % 4) / 2)
}, (_, ii) => {
return [`_${ii}`, `./_${i - ii}`];
})),
}];
})
);
const json = JSON.parse(JSON.stringify({ resources }), (_, o) => {
if (o && typeof o === 'object') {
Reflect.setPrototypeOf(o, null);
Object.freeze(o);
}
return o;
});
const { Manifest } = require('internal/policy/manifest');
bench.start();
for (let i = 0; i < conf.n; i++) {
new Manifest(json, 'file://benchmark/policy-relative');
}
bench.end(conf.n);
}

View File

@ -4,6 +4,7 @@ const {
ArrayIsArray, ArrayIsArray,
Map, Map,
MapPrototypeSet, MapPrototypeSet,
ObjectCreate,
ObjectEntries, ObjectEntries,
ObjectFreeze, ObjectFreeze,
ObjectSetPrototypeOf, ObjectSetPrototypeOf,
@ -29,7 +30,6 @@ const { URL } = require('internal/url');
const { createHash, timingSafeEqual } = crypto; const { createHash, timingSafeEqual } = crypto;
const HashUpdate = uncurryThis(crypto.Hash.prototype.update); const HashUpdate = uncurryThis(crypto.Hash.prototype.update);
const HashDigest = uncurryThis(crypto.Hash.prototype.digest); const HashDigest = uncurryThis(crypto.Hash.prototype.digest);
const BufferEquals = uncurryThis(Buffer.prototype.equals);
const BufferToString = uncurryThis(Buffer.prototype.toString); const BufferToString = uncurryThis(Buffer.prototype.toString);
const kRelativeURLStringPattern = /^\.{0,2}\//; const kRelativeURLStringPattern = /^\.{0,2}\//;
const { getOptionValue } = require('internal/options'); const { getOptionValue } = require('internal/options');
@ -54,9 +54,47 @@ function REACTION_LOG(error) {
} }
class Manifest { class Manifest {
/**
* @type {Map<string, true | string | SRI[]>}
*
* Used to compare a resource to the content body at the resource.
* `true` is used to signify that all integrities are allowed, otherwise,
* SRI strings are parsed to compare with the body.
*
* This stores strings instead of eagerly parsing SRI strings
* and only converts them to SRI data structures when needed.
* This avoids needing to parse all SRI strings at startup even
* if some never end up being used.
*/
#integrities = new SafeMap(); #integrities = new SafeMap();
/**
* @type {Map<string, (specifier: string) => true | URL>}
*
* Used to find where a dependency is located.
*
* This stores functions to lazily calculate locations as needed.
* `true` is used to signify that the location is not specified
* by the manifest and default resolution should be allowed.
*/
#dependencies = new SafeMap(); #dependencies = new SafeMap();
/**
* @type {(err: Error) => void}
*
* Performs default action for what happens when a manifest encounters
* a violation such as abort()ing or exiting the process, throwing the error,
* or logging the error.
*/
#reaction = null; #reaction = null;
/**
* `obj` should match the policy file format described in the docs
* it is expected to not have prototype pollution issues either by reassigning
* the prototype to `null` for values or by running prior to any user code.
*
* `manifestURL` is a URL to resolve relative locations against.
*
* @param {object} obj
* @param {string} manifestURL
*/
constructor(obj, manifestURL) { constructor(obj, manifestURL) {
const integrities = this.#integrities; const integrities = this.#integrities;
const dependencies = this.#dependencies; const dependencies = this.#dependencies;
@ -96,35 +134,14 @@ class Manifest {
let integrity = manifestEntries[i][1].integrity; let integrity = manifestEntries[i][1].integrity;
if (!integrity) integrity = null; if (!integrity) integrity = null;
if (integrity != null) { if (integrity != null) {
debug(`Manifest contains integrity for url ${originalHREF}`); debug('Manifest contains integrity for url %s', originalHREF);
if (typeof integrity === 'string') { if (typeof integrity === 'string') {
const sri = ObjectFreeze(SRI.parse(integrity));
if (integrities.has(resourceHREF)) { if (integrities.has(resourceHREF)) {
const old = integrities.get(resourceHREF); if (integrities.get(resourceHREF) !== integrity) {
let mismatch = false;
if (old.length !== sri.length) {
mismatch = true;
} else {
compare:
for (let sriI = 0; sriI < sri.length; sriI++) {
for (let 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(resourceURL); throw new ERR_MANIFEST_INTEGRITY_MISMATCH(resourceURL);
} }
} }
integrities.set(resourceHREF, sri); integrities.set(resourceHREF, integrity);
} else if (integrity === true) { } else if (integrity === true) {
integrities.set(resourceHREF, true); integrities.set(resourceHREF, true);
} else { } else {
@ -136,7 +153,7 @@ class Manifest {
let dependencyMap = manifestEntries[i][1].dependencies; let dependencyMap = manifestEntries[i][1].dependencies;
if (dependencyMap === null || dependencyMap === undefined) { if (dependencyMap === null || dependencyMap === undefined) {
dependencyMap = {}; dependencyMap = ObjectCreate(null);
} }
if (typeof dependencyMap === 'object' && !ArrayIsArray(dependencyMap)) { if (typeof dependencyMap === 'object' && !ArrayIsArray(dependencyMap)) {
/** /**
@ -198,13 +215,18 @@ class Manifest {
assertIntegrity(url, content) { assertIntegrity(url, content) {
const href = `${url}`; const href = `${url}`;
debug(`Checking integrity of ${href}`); debug('Checking integrity of %s', href);
const integrities = this.#integrities; const integrities = this.#integrities;
const realIntegrities = new Map(); const realIntegrities = new Map();
if (integrities.has(href)) { if (integrities.has(href)) {
const integrityEntries = integrities.get(href); let integrityEntries = integrities.get(href);
if (integrityEntries === true) return true; if (integrityEntries === true) return true;
if (typeof integrityEntries === 'string') {
const sri = ObjectFreeze(SRI.parse(integrityEntries));
integrities.set(href, sri);
integrityEntries = sri;
}
// Avoid clobbered Symbol.iterator // Avoid clobbered Symbol.iterator
for (let i = 0; i < integrityEntries.length; i++) { for (let i = 0; i < integrityEntries.length; i++) {
const { const {

View File

@ -1,17 +1,19 @@
'use strict'; 'use strict';
// Value of https://w3c.github.io/webappsec-subresource-integrity/#the-integrity-attribute // Utility to parse the value of
// https://w3c.github.io/webappsec-subresource-integrity/#the-integrity-attribute
const { const {
ObjectDefineProperty, ObjectDefineProperty,
ObjectFreeze, ObjectFreeze,
ObjectGetPrototypeOf,
ObjectSeal, ObjectSeal,
ObjectSetPrototypeOf,
RegExp, RegExp,
RegExpPrototypeExec, RegExpPrototypeExec,
RegExpPrototypeTest, RegExpPrototypeTest,
StringPrototypeSlice, StringPrototypeSlice,
} = primordials; } = primordials;
// Returns [{algorithm, value (in base64 string), options,}]
const { const {
ERR_SRI_PARSE ERR_SRI_PARSE
} = require('internal/errors').codes; } = require('internal/errors').codes;
@ -21,7 +23,8 @@ const kHASH_ALGO = 'sha(?:256|384|512)';
// Base64 // Base64
const kHASH_VALUE = '[A-Za-z0-9+/]+[=]{0,2}'; const kHASH_VALUE = '[A-Za-z0-9+/]+[=]{0,2}';
const kHASH_EXPRESSION = `(${kHASH_ALGO})-(${kHASH_VALUE})`; const kHASH_EXPRESSION = `(${kHASH_ALGO})-(${kHASH_VALUE})`;
const kOPTION_EXPRESSION = `(${kVCHAR}*)`; // Ungrouped since unused
const kOPTION_EXPRESSION = `(?:${kVCHAR}*)`;
const kHASH_WITH_OPTIONS = `${kHASH_EXPRESSION}(?:[?](${kOPTION_EXPRESSION}))?`; const kHASH_WITH_OPTIONS = `${kHASH_EXPRESSION}(?:[?](${kOPTION_EXPRESSION}))?`;
const kSRIPattern = RegExp(`(${kWSP}*)(?:${kHASH_WITH_OPTIONS})`, 'g'); const kSRIPattern = RegExp(`(${kWSP}*)(?:${kHASH_WITH_OPTIONS})`, 'g');
ObjectSeal(kSRIPattern); ObjectSeal(kSRIPattern);
@ -29,9 +32,10 @@ const kAllWSP = RegExp(`^${kWSP}*$`);
ObjectSeal(kAllWSP); ObjectSeal(kAllWSP);
const BufferFrom = require('buffer').Buffer.from; const BufferFrom = require('buffer').Buffer.from;
const RealArrayPrototype = ObjectGetPrototypeOf([]);
// Returns {algorithm, value (in base64 string), options,}[]
const parse = (str) => { const parse = (str) => {
kSRIPattern.lastIndex = 0;
let prevIndex = 0; let prevIndex = 0;
let match; let match;
const entries = []; const entries = [];
@ -62,7 +66,7 @@ const parse = (str) => {
throw new ERR_SRI_PARSE(str, str.charAt(prevIndex), prevIndex); throw new ERR_SRI_PARSE(str, str.charAt(prevIndex), prevIndex);
} }
} }
return entries; return ObjectSetPrototypeOf(entries, RealArrayPrototype);
}; };
module.exports = { module.exports = {

View File

@ -0,0 +1,9 @@
'use strict';
require('../common');
const runBenchmark = require('../common/benchmark');
runBenchmark('policy', [
'n=1',
]);