1'use strict'; 2 3const { 4 ArrayIsArray, 5 Map, 6 MapPrototypeSet, 7 ObjectCreate, 8 ObjectEntries, 9 ObjectFreeze, 10 ObjectSetPrototypeOf, 11 RegExpPrototypeTest, 12 SafeMap, 13 uncurryThis, 14} = primordials; 15const { 16 canBeRequiredByUsers 17} = require('internal/bootstrap/loaders').NativeModule; 18 19const { 20 ERR_MANIFEST_ASSERT_INTEGRITY, 21 ERR_MANIFEST_INTEGRITY_MISMATCH, 22 ERR_MANIFEST_INVALID_RESOURCE_FIELD, 23 ERR_MANIFEST_UNKNOWN_ONERROR, 24} = require('internal/errors').codes; 25let debug = require('internal/util/debuglog').debuglog('policy', (fn) => { 26 debug = fn; 27}); 28const SRI = require('internal/policy/sri'); 29const crypto = require('crypto'); 30const { Buffer } = require('buffer'); 31const { URL } = require('internal/url'); 32const { createHash, timingSafeEqual } = crypto; 33const HashUpdate = uncurryThis(crypto.Hash.prototype.update); 34const HashDigest = uncurryThis(crypto.Hash.prototype.digest); 35const BufferToString = uncurryThis(Buffer.prototype.toString); 36const kRelativeURLStringPattern = /^\.{0,2}\//; 37const { getOptionValue } = require('internal/options'); 38const shouldAbortOnUncaughtException = 39 getOptionValue('--abort-on-uncaught-exception'); 40const { abort, exit, _rawDebug } = process; 41 42function REACTION_THROW(error) { 43 throw error; 44} 45 46function REACTION_EXIT(error) { 47 REACTION_LOG(error); 48 if (shouldAbortOnUncaughtException) { 49 abort(); 50 } 51 exit(1); 52} 53 54function REACTION_LOG(error) { 55 _rawDebug(error.stack); 56} 57 58class Manifest { 59 /** 60 * @type {Map<string, true | string | SRI[]>} 61 * 62 * Used to compare a resource to the content body at the resource. 63 * `true` is used to signify that all integrities are allowed, otherwise, 64 * SRI strings are parsed to compare with the body. 65 * 66 * This stores strings instead of eagerly parsing SRI strings 67 * and only converts them to SRI data structures when needed. 68 * This avoids needing to parse all SRI strings at startup even 69 * if some never end up being used. 70 */ 71 #integrities = new SafeMap(); 72 /** 73 * @type {Map<string, (specifier: string) => true | URL>} 74 * 75 * Used to find where a dependency is located. 76 * 77 * This stores functions to lazily calculate locations as needed. 78 * `true` is used to signify that the location is not specified 79 * by the manifest and default resolution should be allowed. 80 */ 81 #dependencies = new SafeMap(); 82 /** 83 * @type {(err: Error) => void} 84 * 85 * Performs default action for what happens when a manifest encounters 86 * a violation such as abort()ing or exiting the process, throwing the error, 87 * or logging the error. 88 */ 89 #reaction = null; 90 /** 91 * `obj` should match the policy file format described in the docs 92 * it is expected to not have prototype pollution issues either by reassigning 93 * the prototype to `null` for values or by running prior to any user code. 94 * 95 * `manifestURL` is a URL to resolve relative locations against. 96 * 97 * @param {object} obj 98 * @param {string} manifestURL 99 */ 100 constructor(obj, manifestURL) { 101 const integrities = this.#integrities; 102 const dependencies = this.#dependencies; 103 let reaction = REACTION_THROW; 104 105 if (obj.onerror) { 106 const behavior = obj.onerror; 107 if (behavior === 'throw') { 108 } else if (behavior === 'exit') { 109 reaction = REACTION_EXIT; 110 } else if (behavior === 'log') { 111 reaction = REACTION_LOG; 112 } else { 113 throw new ERR_MANIFEST_UNKNOWN_ONERROR(behavior); 114 } 115 } 116 117 this.#reaction = reaction; 118 const manifestEntries = ObjectEntries(obj.resources); 119 120 const parsedURLs = new SafeMap(); 121 for (let i = 0; i < manifestEntries.length; i++) { 122 let resourceHREF = manifestEntries[i][0]; 123 const originalHREF = resourceHREF; 124 let resourceURL; 125 if (parsedURLs.has(resourceHREF)) { 126 resourceURL = parsedURLs.get(resourceHREF); 127 resourceHREF = resourceURL.href; 128 } else if ( 129 RegExpPrototypeTest(kRelativeURLStringPattern, resourceHREF) 130 ) { 131 resourceURL = new URL(resourceHREF, manifestURL); 132 resourceHREF = resourceURL.href; 133 parsedURLs.set(originalHREF, resourceURL); 134 parsedURLs.set(resourceHREF, resourceURL); 135 } 136 let integrity = manifestEntries[i][1].integrity; 137 if (!integrity) integrity = null; 138 if (integrity != null) { 139 debug('Manifest contains integrity for url %s', originalHREF); 140 if (typeof integrity === 'string') { 141 if (integrities.has(resourceHREF)) { 142 if (integrities.get(resourceHREF) !== integrity) { 143 throw new ERR_MANIFEST_INTEGRITY_MISMATCH(resourceURL); 144 } 145 } 146 integrities.set(resourceHREF, integrity); 147 } else if (integrity === true) { 148 integrities.set(resourceHREF, true); 149 } else { 150 throw new ERR_MANIFEST_INVALID_RESOURCE_FIELD( 151 resourceHREF, 152 'integrity'); 153 } 154 } 155 156 let dependencyMap = manifestEntries[i][1].dependencies; 157 if (dependencyMap === null || dependencyMap === undefined) { 158 dependencyMap = ObjectCreate(null); 159 } 160 if (typeof dependencyMap === 'object' && !ArrayIsArray(dependencyMap)) { 161 /** 162 * @returns {true | URL} 163 */ 164 const dependencyRedirectList = (toSpecifier) => { 165 if (toSpecifier in dependencyMap !== true) { 166 return null; 167 } 168 const to = dependencyMap[toSpecifier]; 169 if (to === true) { 170 return true; 171 } 172 if (parsedURLs.has(to)) { 173 return parsedURLs.get(to); 174 } else if (canBeRequiredByUsers(to)) { 175 const href = `node:${to}`; 176 const resolvedURL = new URL(href); 177 parsedURLs.set(to, resolvedURL); 178 parsedURLs.set(href, resolvedURL); 179 return resolvedURL; 180 } else if (RegExpPrototypeTest(kRelativeURLStringPattern, to)) { 181 const resolvedURL = new URL(to, manifestURL); 182 const href = resourceURL.href; 183 parsedURLs.set(to, resolvedURL); 184 parsedURLs.set(href, resolvedURL); 185 return resolvedURL; 186 } 187 const resolvedURL = new URL(to); 188 const href = resourceURL.href; 189 parsedURLs.set(to, resolvedURL); 190 parsedURLs.set(href, resolvedURL); 191 return resolvedURL; 192 }; 193 dependencies.set(resourceHREF, dependencyRedirectList); 194 } else if (dependencyMap === true) { 195 const arbitraryDependencies = () => true; 196 dependencies.set(resourceHREF, arbitraryDependencies); 197 } else { 198 throw new ERR_MANIFEST_INVALID_RESOURCE_FIELD( 199 resourceHREF, 200 'dependencies'); 201 } 202 } 203 ObjectFreeze(this); 204 } 205 206 getRedirector(requester) { 207 requester = `${requester}`; 208 const dependencies = this.#dependencies; 209 if (dependencies.has(requester)) { 210 return { 211 resolve: (to) => dependencies.get(requester)(`${to}`), 212 reaction: this.#reaction 213 }; 214 } 215 return null; 216 } 217 218 assertIntegrity(url, content) { 219 const href = `${url}`; 220 debug('Checking integrity of %s', href); 221 const integrities = this.#integrities; 222 const realIntegrities = new Map(); 223 224 if (integrities.has(href)) { 225 let integrityEntries = integrities.get(href); 226 if (integrityEntries === true) return true; 227 if (typeof integrityEntries === 'string') { 228 const sri = ObjectFreeze(SRI.parse(integrityEntries)); 229 integrities.set(href, sri); 230 integrityEntries = sri; 231 } 232 // Avoid clobbered Symbol.iterator 233 for (let i = 0; i < integrityEntries.length; i++) { 234 const { 235 algorithm, 236 value: expected 237 } = integrityEntries[i]; 238 const hash = createHash(algorithm); 239 HashUpdate(hash, content); 240 const digest = HashDigest(hash); 241 if (digest.length === expected.length && 242 timingSafeEqual(digest, expected)) { 243 return true; 244 } 245 MapPrototypeSet( 246 realIntegrities, 247 algorithm, 248 BufferToString(digest, 'base64') 249 ); 250 } 251 } 252 const error = new ERR_MANIFEST_ASSERT_INTEGRITY(url, realIntegrities); 253 this.#reaction(error); 254 } 255} 256 257// Lock everything down to avoid problems even if reference is leaked somehow 258ObjectSetPrototypeOf(Manifest, null); 259ObjectSetPrototypeOf(Manifest.prototype, null); 260ObjectFreeze(Manifest); 261ObjectFreeze(Manifest.prototype); 262module.exports = ObjectFreeze({ Manifest }); 263