@sjcrh/proteinpaint-server 2.213.0 → 2.214.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,6 +46,13 @@ if (!serverconfigfile) {
46
46
  }
47
47
  }
48
48
 
49
+ // an absent top-level setting must read as undefined, instead of from a polluted Object.prototype,
50
+ // e.g. `if (serverconfig.debugmode)` when debugmode is not set in a prod serverconfig.json;
51
+ // done right after parsing so that every setting read and default applied below in this module is also covered,
52
+ // and in place so that it applies to every module that imports serverconfig;
53
+ // the whole object is frozen later by lockServerconfig() in app.ts launch(), not here, since launch code and tests still modify it
54
+ Object.setPrototypeOf(serverconfig, null)
55
+
49
56
  // Derived settings from a launcher script, such as the container app-server.mjs and app-full.mjs,
50
57
  // applied as if these were in serverconfig.json. This avoids rewriting a mounted serverconfig.json,
51
58
  // which may be read-only or not writable by the container user. Applied before any other processing,
@@ -385,6 +392,96 @@ if (!serverconfig.cache_snpgt) {
385
392
 
386
393
  export default serverconfig
387
394
 
395
+ /*
396
+ Make the whole serverconfig{} immutable for the rest of the server process lifetime, so that a request handler,
397
+ whether by bug or a malicious payload, cannot change a setting, e.g. turn on a dev-only or dangerous feature.
398
+
399
+ Must be called by app.ts launch() as soon as all launch-time writes are done, which are:
400
+ - the defaults and derived settings applied above in this module
401
+ - the removal of serverconfig.dsCredentials by auth.ts extractValidatedCreds()
402
+ - initGenomesDs(), which normalizes some settings, e.g. base_zindex, modifies serverconfig.genomes[] entries or
403
+ their objects that are shared with the in-memory genome objects, e.g. snp{} and the raw dataset entries,
404
+ and calls mergeDsFeatures() on the first, awaited dataset init attempt;
405
+ dataset init retries after listen() only read serverconfig
406
+ Do not call this when serverconfig.js is imported, since many unit tests modify serverconfig.
407
+
408
+ - the whole object and everything nested in it are frozen, e.g. serverconfig.genomes[] and allowedEmbedders[],
409
+ including the objects that are shared with in-memory genome objects, which are then also read-only
410
+ - features{} and every plain object nested in it also get a null prototype, so that a flag check such as
411
+ `if (serverconfig.features.loosenCORS)` or `if (serverconfig.features.wsi?.allowDirectSlidePath)`
412
+ cannot read a value from a polluted Object.prototype; arrays keep their prototype for .includes() etc;
413
+ the top-level serverconfig{} already has a null prototype, as set above right after parsing
414
+
415
+ objects are locked in place instead of copied, so that a reference that a module captured at import time,
416
+ e.g. `const bamCache = serverconfig.features.bamCache` in bam.js, is locked too
417
+
418
+ throws, to fail the launch, when a value cannot be fully locked
419
+ */
420
+ export function lockServerconfig(sc) {
421
+ const visited = new WeakSet()
422
+ // lock features{} first, so that its objects get a null prototype before the whole-object pass visits them
423
+ deepLock(sc.features, 'serverconfig.features', visited, true)
424
+ deepLock(sc, 'serverconfig', visited, false)
425
+ }
426
+
427
+ /*
428
+ obj: the value to lock
429
+ keyPath: for error messages
430
+ visited: to handle a circular or repeated reference
431
+ nullProto: true to also set a null prototype on a plain object
432
+ */
433
+ function deepLock(obj, keyPath, visited, nullProto) {
434
+ // like a getter, a function may return a different value on each call, which freezing cannot prevent
435
+ if (typeof obj == 'function') throw `${keyPath} cannot be locked, since it is a function`
436
+ if (!obj || typeof obj != 'object' || visited.has(obj)) return
437
+ visited.add(obj)
438
+ const proto = Object.getPrototypeOf(obj)
439
+ if (obj instanceof RegExp) {
440
+ // test() and exec() on a regex with the g or y flag write to its lastIndex, which throws when frozen
441
+ if (obj.global || obj.sticky) throw `${keyPath} cannot be locked, since it is a RegExp with the g or y flag`
442
+ } else if (proto !== Object.prototype && proto !== null && !Array.isArray(obj)) {
443
+ // e.g. a Map, Set, or Date, whose contents are not protected by Object.freeze()
444
+ throw `${keyPath} cannot be locked, since only plain objects, arrays, and RegExps are supported`
445
+ } else if (nullProto && proto === Object.prototype) {
446
+ // the prototype of a frozen, sealed, or otherwise non-extensible object cannot be changed,
447
+ // so an object that was already frozen by other code, e.g. a dataset, would keep inheriting from Object.prototype
448
+ if (!Object.isExtensible(obj)) throw `${keyPath} cannot be locked, since it is already non-extensible`
449
+ Object.setPrototypeOf(obj, null)
450
+ }
451
+ Object.freeze(obj)
452
+ // always traverse, since an object that was already frozen by other code may still have mutable descendants;
453
+ // use all own keys, not just enumerable string keys, so that no nested value is skipped
454
+ for (const key of Reflect.ownKeys(obj)) {
455
+ const d = Object.getOwnPropertyDescriptor(obj, key)
456
+ // a getter may return a different or mutable value on each call, which freezing cannot prevent
457
+ if (!('value' in d)) throw `${keyPath}.${String(key)} cannot be locked, since it is a getter/setter property`
458
+ deepLock(d.value, `${keyPath}.${String(key)}`, visited, nullProto)
459
+ }
460
+ }
461
+
462
+ /*
463
+ Copy the optional ds.serverconfigFeatures{} into serverconfig.features{}, for keys that are not already set.
464
+ Overwrite not allowed! to prevent hard-to-trace error that 2nd ds changes value set by 1st ds etc...
465
+
466
+ Only own keys are copied and checked, so that a key inherited from a polluted Object.prototype
467
+ is never copied as an own feature, which lockServerconfig() would then freeze as enabled.
468
+ */
469
+ export function mergeDsFeatures(sc, ds) {
470
+ for (const k of Object.keys(ds.serverconfigFeatures || {})) {
471
+ if (Object.hasOwn(sc.features, k)) {
472
+ // on init retry, no need to see this message
473
+ if (!ds.init?.status) console.log(`!!! NO OVERWRITING SERVERCONFIG.FEATURES.${k} (from ${ds.label}) !!!`)
474
+ } else {
475
+ Object.defineProperty(sc.features, k, {
476
+ value: ds.serverconfigFeatures[k],
477
+ enumerable: true,
478
+ writable: true,
479
+ configurable: true
480
+ })
481
+ }
482
+ }
483
+ }
484
+
388
485
  /*
389
486
  Option to add datasets under hg38-test and also feature flags, dsCredentials
390
487