• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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