|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +// The four CryptoKey prototype getters (`type`, `extractable`, |
| 4 | +// `algorithm`, `usages`) are user-configurable per Web IDL, so they |
| 5 | +// can be invoked with an arbitrary `this`. The native callbacks that |
| 6 | +// implement them must brand-check their receiver and throw cleanly |
| 7 | +// (ERR_INVALID_THIS) rather than crashing the process or returning |
| 8 | +// garbage. This test exercises four progressively more hostile |
| 9 | +// receiver shapes, including subverting `instanceof` via |
| 10 | +// `Symbol.hasInstance`, to make sure the C++ brand check holds. |
| 11 | +// |
| 12 | +// It also verifies that `util.types.isCryptoKey()` cannot be fooled |
| 13 | +// by prototype spoofing. |
| 14 | + |
| 15 | +const common = require('../common'); |
| 16 | +if (!common.hasCrypto) |
| 17 | + common.skip('missing crypto'); |
| 18 | + |
| 19 | +const assert = require('node:assert'); |
| 20 | +const { types: { isCryptoKey } } = require('node:util'); |
| 21 | +const { subtle } = globalThis.crypto; |
| 22 | + |
| 23 | +(async () => { |
| 24 | + const key = await subtle.generateKey( |
| 25 | + { name: 'HMAC', hash: 'SHA-256' }, |
| 26 | + true, |
| 27 | + ['sign'], |
| 28 | + ); |
| 29 | + |
| 30 | + const CryptoKey = key.constructor; |
| 31 | + |
| 32 | + // Capture the underlying prototype getters once, so that subsequent |
| 33 | + // tampering with `CryptoKey.prototype` cannot affect what we call. |
| 34 | + const getters = { |
| 35 | + type: Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'type').get, |
| 36 | + extractable: |
| 37 | + Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'extractable').get, |
| 38 | + algorithm: |
| 39 | + Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'algorithm').get, |
| 40 | + usages: |
| 41 | + Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'usages').get, |
| 42 | + }; |
| 43 | + |
| 44 | + // Sanity: each getter works on a real CryptoKey. |
| 45 | + Object.entries(getters).forEach(([name, getter]) => { |
| 46 | + assert.notStrictEqual(getter.call(key), undefined, `baseline ${name}`); |
| 47 | + }); |
| 48 | + assert.strictEqual(isCryptoKey(key), true); |
| 49 | + |
| 50 | + const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' }; |
| 51 | + |
| 52 | + // Plain object receiver. |
| 53 | + Object.entries(getters).forEach(([, getter]) => { |
| 54 | + assert.throws(() => getter.call({}), invalidThis); |
| 55 | + }); |
| 56 | + |
| 57 | + // Null-prototype object receiver. |
| 58 | + Object.entries(getters).forEach(([, getter]) => { |
| 59 | + assert.throws(() => getter.call({ __proto__: null }), invalidThis); |
| 60 | + }); |
| 61 | + |
| 62 | + // Primitive receiver. |
| 63 | + Object.entries(getters).forEach(([, getter]) => { |
| 64 | + assert.throws(() => getter.call(1), invalidThis); |
| 65 | + }); |
| 66 | + |
| 67 | + // Null. |
| 68 | + Object.entries(getters).forEach(([, getter]) => { |
| 69 | + // eslint-disable-next-line no-useless-call |
| 70 | + assert.throws(() => getter.call(null), invalidThis); |
| 71 | + }); |
| 72 | + |
| 73 | + // Undefined. |
| 74 | + Object.entries(getters).forEach(([, getter]) => { |
| 75 | + assert.throws(() => getter.call(), invalidThis); |
| 76 | + }); |
| 77 | + |
| 78 | + // Function |
| 79 | + Object.entries(getters).forEach(([, getter]) => { |
| 80 | + assert.throws(() => getter.call(function() {}), invalidThis); |
| 81 | + }); |
| 82 | + |
| 83 | + // Prototype spoofing with InternalCryptoKey.prototype must not pass |
| 84 | + // util.types.isCryptoKey(). |
| 85 | + const spoofed = {}; |
| 86 | + Object.setPrototypeOf(spoofed, Object.getPrototypeOf(key)); |
| 87 | + assert.strictEqual(spoofed instanceof CryptoKey, true); |
| 88 | + assert.strictEqual(isCryptoKey(spoofed), false); |
| 89 | + await assert.rejects( |
| 90 | + subtle.sign('HMAC', spoofed, Buffer.from('payload')), |
| 91 | + invalidThis); |
| 92 | + await assert.rejects( |
| 93 | + subtle.exportKey('jwk', spoofed), |
| 94 | + invalidThis); |
| 95 | + |
| 96 | + // Subvert `instanceof CryptoKey` via Symbol.hasInstance, then |
| 97 | + // invoke the native getters on a forged object. The C++ tag |
| 98 | + // check must reject the receiver even though `instanceof` |
| 99 | + // reports true. |
| 100 | + Object.defineProperty(CryptoKey, Symbol.hasInstance, { |
| 101 | + configurable: true, |
| 102 | + value: () => true, |
| 103 | + }); |
| 104 | + const fake = { foo: 'bar' }; |
| 105 | + assert.strictEqual(fake instanceof CryptoKey, true); |
| 106 | + assert.strictEqual(isCryptoKey(fake), false); |
| 107 | + Object.entries(getters).forEach(([, getter]) => { |
| 108 | + assert.throws(() => getter.call(fake), invalidThis); |
| 109 | + }); |
| 110 | + |
| 111 | + // Subverted `instanceof` plus a real BaseObject of a different |
| 112 | + // kind (a Buffer) as the receiver. Without the C++ tag check |
| 113 | + // this would type-confuse `Unwrap<NativeCryptoKey>`. |
| 114 | + const buf = Buffer.alloc(16); |
| 115 | + assert.strictEqual(buf instanceof CryptoKey, true); |
| 116 | + assert.strictEqual(isCryptoKey(buf), false); |
| 117 | + Object.entries(getters).forEach(([, getter]) => { |
| 118 | + assert.throws(() => getter.call(buf), invalidThis); |
| 119 | + }); |
| 120 | + |
| 121 | + // The real CryptoKey continues to work after all of the above. |
| 122 | + assert.strictEqual(getters.type.call(key), 'secret'); |
| 123 | + assert.strictEqual(getters.extractable.call(key), true); |
| 124 | + assert.strictEqual(getters.algorithm.call(key).name, 'HMAC'); |
| 125 | + assert.deepStrictEqual(getters.usages.call(key), ['sign']); |
| 126 | +})().then(common.mustCall()); |
0 commit comments