|
| 1 | +# Input Validation for ERTP - A Walkthrough |
| 2 | + |
| 3 | +## Requirements |
| 4 | + |
| 5 | +We want ERTP to be robust against malicious code in the same vat, so the ERTP must be robust without the kind of checks that we get for free in intervat communication going through `@agoric/marshal` and `@agoric/swingset-vat`. In other words, the only tools at our disposal are being in a Hardened JS environment, and helpers that we import and call directly, such as `passStyleOf` from `@agoric/marshal`. |
| 6 | + |
| 7 | +## Malicious Behavior |
| 8 | + |
| 9 | +We identify two personas: the creator of an issuerKit, and a user of the asset type. A user is someone who holds a purse or payment of that brand, or describes something in terms of an amount of that brand. A creator might be vulnerable to an attack by users and vice versa. Users might be vulnerable to other users. |
| 10 | + |
| 11 | +The malicious behavior we would like to prevent is: |
| 12 | + |
| 13 | +1. Users stealing funds from other users |
| 14 | +2. Users minting who do not have access to the mint |
| 15 | +3. The creator of an issuerKit revoking assets from the holders of purses and payments |
| 16 | + |
| 17 | +## Entry points in ERTP |
| 18 | + |
| 19 | +There are three entry points in ERTP: |
| 20 | +1. packages/ERTP/src/amountMath.js |
| 21 | +2. packages/ERTP/src/issuerKit.js |
| 22 | +3. packages/ERTP/src/typeGuards.js |
| 23 | + |
| 24 | +This document will only analyze `AmountMath.coerce`, but the |
| 25 | +methodology for validating inputs remains the same for the ERTP functions. |
| 26 | + |
| 27 | +`AmountMath.coerce` takes a `brand` and an `amount` and returns a new `amount`, but throws if the amount does not match the brand, or if the amount is not a valid amount generally. |
| 28 | + |
| 29 | +For `coerce` to work according to spec, if the user passes in an invalid brand or an invalid amount, we must throw. |
| 30 | + |
| 31 | +## Valid amounts |
| 32 | + |
| 33 | +A valid amount is an object record with two properties: `brand` and |
| 34 | +`value`. `brand` is a Remotable with a few methods, such as |
| 35 | +`isMyIssuer`, but for the purposes of a valid amount, we allow any |
| 36 | +Remotable and do not call any of these methods (we would have to call |
| 37 | +them asynchronously in order to check them, since the `brand` might be |
| 38 | +a remote object, so we do not check them.) |
| 39 | + |
| 40 | +The value is either a bigint Nat in the case of AssetKind.NAT (we formerly allowed numbers for backwards compatibility, but now only bigints are allowed) or an array of Structures in the case of AssetKind.SET. The array of Structures might include any combination of other Structures, “undefined”, “null”, booleans, symbols, strings, pass-by-copy records, pass-by-copy arrays, numbers, bigints, Remotables, and errors. Importantly, a Structure cannot contain promises. |
| 41 | + |
| 42 | +## Invalid Amounts |
| 43 | + |
| 44 | +There are a number of ways in which amounts can be invalid: |
| 45 | + |
| 46 | +### Not an object |
| 47 | + |
| 48 | +* **The danger**: we’ll get unexpected failures later on in our use of the amount rather than failing fast at the start as intended. |
| 49 | + |
| 50 | +### Not a CopyRecord |
| 51 | + |
| 52 | +* **The danger**: an object that isn’t a `copyRecord` can have `getters` that return different values at different times, so checking the value is no guarantee that the value will be the same when accessed later. For example: |
| 53 | + |
| 54 | +```js |
| 55 | +const object1 = {}; |
| 56 | +let checked = false; |
| 57 | +Object.defineProperty(object1, 'value', { |
| 58 | + get() { |
| 59 | + if (checked) { |
| 60 | + return 1000000n; |
| 61 | + } else { |
| 62 | + checked = true; |
| 63 | + return 1n; |
| 64 | + } |
| 65 | + }, |
| 66 | +}); |
| 67 | +``` |
| 68 | +```sh |
| 69 | +> object1.value |
| 70 | +1n |
| 71 | +> object1.value |
| 72 | +1000000n |
| 73 | +``` |
| 74 | + |
| 75 | +`Object.freeze(object1)` is not helpful here, as the `getter` is not changing, the value that is being returned is. `harden` also does not prevent `value` from changing. When harden does encounter an accessor property during its traversal, it does not "read" the property, and thereby does not call the getter. Rather, it proceeds to recursively harden the getter and setter themselves. That's because, for an accessor property, its current value isn't, for this purpose, considered part of its API surface, but rather part of the behavior of that API. Thus, we need an additional way to ensure that the value of `checked` cannot change. |
| 76 | + |
| 77 | +`isPassStyleOf` throws on objects with accessor properties like the object defined above. |
| 78 | + |
| 79 | +### Is a CopyRecord and is a proxy |
| 80 | + |
| 81 | +* **The dangers**: |
| 82 | + 1) A proxy can throw on any property access, but only the first |
| 83 | + time. Any time it does *not* throw, it *must* return the same |
| 84 | + value as before. |
| 85 | + 2) A proxy can mount a reentrancy attack where a proxy handler is running while code is executing, and can reenter the code while the original is still executing. |
| 86 | + |
| 87 | +Commit points are a good mitigation for #1 - do all of your checks upfront so that any throwing causes you to fail fast, before any state is changed. |
| 88 | + |
| 89 | +Some mitigation for #2 are: |
| 90 | +1. Create a new object with a spread operator so that the proxy isn't |
| 91 | + used further. |
| 92 | +2. Destructure and then never use the original object again. |
| 93 | +3. Use `pureCopy`. `pureCopy` ensures the object is not a proxy, but `pureCopy` only works for copyRecords that are pure data, meaning no remotables and no promises. |
| 94 | + |
| 95 | +We aim to address proxy-based reentrancy by other, lower-level means, thus making `passStyleOf(amount) === ‘copyRecord’` sufficient protection against proxy-based reentrancy as well, but we should note that proxy-based reentrancy is not a threat across a vat boundary because it requires synchronous access. |
| 96 | + |
| 97 | +## Invalid brands |
| 98 | + |
| 99 | +A brand can be "wrong" if: |
| 100 | +1. It's not a Remotable |
| 101 | +2. Its methods don’t adhere to the expected Brand API |
| 102 | +3. It misbehaves (i.e. answering isMyIssuer with different responses) |
| 103 | + |
| 104 | +In determining whether a brand is "valid", we only check that the brand is a remotable. This means that a brand |
| 105 | +that passes our input validation could still have the wrong API or misbehave. |
| 106 | + |
| 107 | +## Who hardens? |
| 108 | +It is the responsibility of the sender/client (the creator of an |
| 109 | +issuerKit or user of ERTP) to harden. AmountMath does not harden |
| 110 | +inputs. |
| 111 | + |
| 112 | +## The implementation |
| 113 | + |
| 114 | +In `AmountMath.coerce(brand, allegedAmount)`, we do the following: |
| 115 | +1. assert that the `brand` is a remotable |
| 116 | +2. assert that the `allegedAmount` is a `copyRecord` |
| 117 | +3. destructure the `allegedAmount` into `allegedBrand` and |
| 118 | + `allegedValue` |
| 119 | +4. Assert that the `brand` is identical to the `allegedBrand` |
| 120 | +5. Call `AmountMath.make(brand, allegedValue)`, which: |
| 121 | + * Asserts that the `brand` is a remotable, again. |
| 122 | + * Asserts that the `allegedValue` is a `copyArray` or a `bigint` |
| 123 | + * Gets the appropriate MathHelpers |
| 124 | + * Calls `helpers.doCoerce(allegedValue)`, which either asserts |
| 125 | + that the value is a `Nat bigint` or that the value is a |
| 126 | + `copyArray` `structure` with no duplicate elements |
| 127 | +11. Return a new `amount` |
| 128 | + |
| 129 | +Thus, we ensure that the `brand` is valid by checking that it is a |
| 130 | +remotable. We ensure that the `allegedAmount` is a copyRecord with |
| 131 | +valid `brand` and `value` properties. |
| 132 | + |
| 133 | +If `allegedAmount` were a proxy, we would either throw in step 3 (the |
| 134 | +destructuring), or we successfully get both the `allegedBrand` and |
| 135 | +`allegedValue` and never touch the proxy again. Currently, we do not |
| 136 | +attempt to prevent proxy-based reentrancy, so this is the full extent |
| 137 | +of our defense. |
| 138 | + |
| 139 | + |
| 140 | + |
| 141 | + |
0 commit comments