|
1 | 1 | # harden |
2 | 2 |
|
3 | | -This `@endo/harden` package is a skeleton package. |
| 3 | +Hardened modules are modules that make their exports resist tampering by other |
| 4 | +modules that import them, making them less suceptible to supply chain attack. |
| 5 | +In [HardenedJS](https://hardenedjs.org), the global `harden` function |
| 6 | +transitively freezes an object and all of the objects that are reachable by |
| 7 | +walking up chains of properties and prototypes. |
| 8 | +All the primordials like `Array.prototype` and `Object` are frozen in |
| 9 | +this environment, which gives your module a place to stand toward its own |
| 10 | +defense. |
| 11 | +Then, with [LavaMoat](https://github.com/lavamoat/lavamoat), each package |
| 12 | +is credibly isolated and only receives the subset of globals and host modules |
| 13 | +it needs to function. |
| 14 | +That is, we can enforce [Principle of Least |
| 15 | +Authority](https://en.wikipedia.org/wiki/Principle_of_least_privilege). |
| 16 | +But, that leaves the module to use `harden` to freeze all its exports and |
| 17 | +anything it returns that might be shared by other packages that use it. |
| 18 | + |
| 19 | +In order to provide type information about the global `harden` in lockded-down |
| 20 | +HardenedJS, and also to make it possible for hardened modules to be used |
| 21 | +outside HardenedJS, the `@endo/harden` package exports a `harden` function that |
| 22 | +can be used either way. |
| 23 | + |
| 24 | +```js |
| 25 | +import { harden } from '@endo/harden'; |
| 26 | + |
| 27 | +export const myFunction = () => {}; |
| 28 | +harden(myFunction); |
| 29 | +``` |
| 30 | + |
| 31 | +By avoiding the export of hoisted `function` and `var` declarations and by |
| 32 | +immediately calling `harden` on any exposed function (or prototype thereof!) we |
| 33 | +leave no window of opportunity for another module to alter our exports. |
| 34 | +If a function's return value is meant to be shared by multiple parties (such |
| 35 | +as memoized objects), a hardened module author should harden the value before |
| 36 | +the function returns it (`return harden(value);`). |
| 37 | + |
| 38 | +# With HardenedJS |
| 39 | + |
| 40 | +The package `@endo/harden` reexports the `globalThis.harden` or |
| 41 | +`Object[Symbol.for('harden')]` in its execution environment, in order of |
| 42 | +preference, and is suitable regardless of whether a module is used |
| 43 | +with or without HardenedJS. |
| 44 | + |
| 45 | +When using SES, `lockdown` creates `globalThis.harden` in the Realm's |
| 46 | +intrinsic `globalThis` and also automatically endows `globalThis.harden` |
| 47 | +to any `Compartment`. |
| 48 | +It is possible to delete `globalThis.harden` on new compartments. |
| 49 | +However, every version of SES published since the introduction of `@endo/harden` |
| 50 | +also provides `Object[Symbol.for('harden')]`, which is a property of one |
| 51 | +of the hardened shared intrinsics and cannot be subverted in a compartment. |
| 52 | + |
| 53 | +The `harden` in `@endo/harden` prefers `globalThis.harden` because this |
| 54 | +affords the greatest degree of flexibility. |
| 55 | +Any multi-tenant `Compartment` should freeze its own `globalThis`, including |
| 56 | +making `harden` non-configurable and non-writable, so there is no risk |
| 57 | +of tampering, and endowing a `Compartment` with a different `harden` |
| 58 | +than the Realm's `Object[Symbol.for('harden')]` may be useful for some |
| 59 | +cases. |
| 60 | + |
| 61 | +When creating a bundle for an application that can safely assume it will run in |
| 62 | +a HardenedJS environment, consider passing the build condition `-C hardened`. |
| 63 | +This will provide the smallest version of `@endo/harden`, one which will throw |
| 64 | +an exception if `harden` is not present. |
| 65 | + |
| 66 | +``` |
| 67 | +bundle-source -C hardened entry.js > entry.json |
| 68 | +``` |
| 69 | + |
| 70 | +# Without HardenedJS |
| 71 | + |
| 72 | +Libraries that use `@endo/harden` can be used without HardenedJS and the |
| 73 | +exported `harden` only freezes the transitive owned properties of the object |
| 74 | +and does not traverse prototype chains. |
| 75 | + |
| 76 | +Consequently, the surface of an object is immutable. |
| 77 | +However, if any fields of an object are optional, an attacker can subvert them |
| 78 | +by altering their prototype. |
| 79 | +This provides a degree of immutability that is useful for partial safety and |
| 80 | +does not interfere with uncoordinated alteration of the realm intrinsics, on |
| 81 | +which some testing and frontend user interface frameworks rely. |
| 82 | + |
| 83 | +To opt out of any safety guarantees and to avoid the computation cost of |
| 84 | +transitively hardening own properties, use the `-C harden:unsafe` build |
| 85 | +condition with tools like `node` and Endo's `bundle-source`. |
| 86 | + |
| 87 | +# Multiple instances |
| 88 | + |
| 89 | +The first instance of `@endo/harden` will determine the behavior of any |
| 90 | +subsequent instance of `@endo/harden` that initializes later, regardless of |
| 91 | +differences in behavior. |
| 92 | +In a mutable, pre-lockdown JavaScript environment, it does this by behaving |
| 93 | +somewhat like a shim. |
| 94 | +A side-effect of the _first use_ of `harden` is that it installs its flavor of |
| 95 | +`harden` at `Object[Symbol.for('harden')]` and all subsequent initializations |
| 96 | +just adopt that behavior. |
| 97 | +This property is how `lockdown` senses that it should fail. |
| 98 | + |
| 99 | +# With _or_ Without _not_ Both |
| 100 | + |
| 101 | +Hardened modules calling `harden` should be fine at any time in an application |
| 102 | +that never uses HardenedJS, calling `lockdown`. |
| 103 | + |
| 104 | +However, initializing a hardened module before setting up a HardenedJS |
| 105 | +environment (before calling `lockdown`) and then proceeding on the assumption |
| 106 | +that it's hardened after `lockdown` would leave the apparently-hardened module |
| 107 | +vulnerable. |
| 108 | + |
| 109 | +So, `@endo/harden` arranges for `lockdown()` to throw an exception with |
| 110 | +a _helpful_ stack if `harden` gets called before `lockdown`. |
| 111 | +The stack points to the module that was initialized before `lockdown` |
| 112 | +and which should be moved after `lockdown`. |
| 113 | +The `lockdown` call often occurs as a side-effect of initializing |
| 114 | +`@endo/lockdown`, `@endo/init`, or by convention, modules with names like |
| 115 | +`prepare-*`. |
| 116 | + |
| 117 | +# Configurability of Compartment harden |
| 118 | + |
| 119 | +The `harden` exported by `@endo/harden` prefers `Object[Symbol.for('harden')]` |
| 120 | +over `globalThis.harden` since the former is an intrinsic that cannot be |
| 121 | +overridden by an endowment. |
| 122 | +Any code that relies on `globalThis.harden` being endowed with a different |
| 123 | +behavior than `Object[Symbol.for('harden')]` cannot substitute `@endo/harden`. |
| 124 | + |
| 125 | +# isFake |
| 126 | + |
| 127 | +Using `lockdown` with the `"unsafe"` `hardenTaming` option creates an environment |
| 128 | +where `Object.isFrozen`, `Object.isExtensible`, `Reflect.isExtensible`, and |
| 129 | +`isSealed` all misreport that any object is frozen, non-extensible, and sealed. |
| 130 | +To indicate this, `harden.isFake` is `true`. |
| 131 | + |
| 132 | +We regret this misfeature. |
| 133 | +The `@endo/harden` does not provide `harden.isFake`. |
| 134 | +Code, especially tests, migrating to use `@endo/harden` should refactor |
| 135 | +`harden.isFake` to use a more legible indicator of the misbehavior of `isFrozen` |
| 136 | +and its compatriots, which may not be indicated by empirical behavior of `harden`. |
| 137 | + |
| 138 | +For example, `Object.isFrozen({})` when `harden.isFake` and more clearly |
| 139 | +conveys the reason a test might be invalidated by `unsafe` `hardenTaming`. |
| 140 | +Testing that the outcome of `Object.isFrozen({})` is the same as the outcome of |
| 141 | +`Object.isFrozen(object)` for an object that should not be frozen makes a test |
| 142 | +work just as well between `safe` and `unsafe` `hardenTaming`. |
| 143 | + |
| 144 | +The module `@endo/harden/is-noop.js` provides `hardenIsNoop(harden)` to |
| 145 | +indicate that `harden` is a no-op, regardless of `hardenTaming`. |
| 146 | +Do not rely on `Object.isFrozen({})` to imply that `harden` is a no-op. |
| 147 | + |
| 148 | +```js |
| 149 | +import harden from '@endo/harden'; |
| 150 | +import hardenIsNoop from '@endo/harden-is-noop.js'; |
| 151 | + |
| 152 | +if (hardenIsNoop(harden)) { |
| 153 | + // ... |
| 154 | +} |
| 155 | +``` |
| 156 | + |
0 commit comments