Repository navigation
fix(node_crypto): size cipher updates by byte length - #36459
Conversation
bartlomieju
left a comment
There was a problem hiding this comment.
Two things:
Test the multi-byte case without spoofing. The typed-array case currently goes through Object.defineProperty(uint16WithSpoofedLength, "length", { value: 1 }), which proves resilience against a shadowed getter — a weaker and less likely claim than the one that matters. A plain new Uint16Array([1, 2, 3]) triggers the bug on its own: byteLength is 6, .length is 3.
Worth spelling out what that case did before the fix, because it's stronger than a sizing mistake. Buffer.allocUnsafe(buf.length) allocated 3 bytes, the op received the full 6 via #[buffer] input: &[u8], and ext/node_crypto/lib.rs:683 hit if output.len() < input.len() { return false; } and returned without calling context.encrypt. The old code discarded that bool, so update() returned the buffer untouched — i.e. uninitialized Buffer.allocUnsafe pool memory handed back as ciphertext, no exception raised. An unspoofed Uint16Array round-trip asserting decrypted === plaintext pins exactly that, and would fail loudly on the old code. Probably worth deciding whether this wants a backport too.
getArrayBufferViewParts is the third copy of this helper. getViewParts at ext/node/polyfills/internal/crypto/diffiehellman.ts:77-92 is structurally identical — same isTypedArray discrimination, same three primordial pairs, only the field names differ (ab/off/len vs buffer/byteOffset/byteLength). Both live under internal/crypto/. The same logic is inlined a third time at _tls_common.ts:406-417, and there's already a shared arrayBufferViewToUint8Array that fs.ts and _fs_read.ts pull from.
Could this be extracted once and reused rather than added again? Three near-identical copies of buffer normalization across the crypto polyfills is where a subtle divergence eventually hides.
Minor, related: getArrayBufferViewByteLength calls getArrayBufferViewParts and throws away two fields, then toFastBufferView recomputes the same parts, then _cache.add(buf) computes them a third time for the same input. Threading one getArrayBufferViewParts(data) result through would be tidier.
bartlomieju
left a comment
There was a problem hiding this comment.
Good find, and it's worse than "wrong size" — the old path leaked uninitialized memory.
For a non-Uint8Array view, buf.length is the element count, so Buffer.allocUnsafe(buf.length) under-allocated: new Uint16Array(32) gave a 32-byte output for 64 bytes of input. op_node_cipheriv_encrypt does guard that (if output.len() < input.len() { return false }), so there was no overflow — but the JS side ignored the return value, so update() handed the caller the allocUnsafe buffer with nothing written into it. That's raw heap memory returned from a crypto API. Same shape on the decipher side.
The fix addresses all three parts of that:
- sizing off
byteLengthvia the primordial getters, so element-count views are correct and a spoofed.lengthown-property can't influence it; toFastBufferViewnormalizing the view so#[buffer] input: &[u8]sees the right bytes for aDataViewor an offset view rather than whateverbyteOffset-less coercion happened before;- checking the op's boolean and throwing instead of returning the buffer, plus
new FastBuffer(n)(zero-filled) rather thanallocUnsafe, which closes the disclosure even if some future path forgets the check.
BlockModeCache.add taking ArrayBufferView and copying through an explicit new Uint8Array(buffer, byteOffset, byteLength) is the matching fix for the block-cache path.
The test's spoofed-length Object.defineProperty cases are exactly right for pinning the primordial usage. LGTM.
## What changed - Normalize `ArrayBufferView` inputs using their underlying buffer, byte offset, and byte length. - Allocate cipher update output with the captured `FastBuffer` constructor. - Keep block-mode caching byte-oriented and handle rejected native updates. - Cover `Buffer`, multi-byte typed-array, `DataView`, and public allocator replacement cases. ## Why Cipher updates consume bytes, while some typed-array properties describe element counts and public `Buffer` allocation methods can be replaced by application code. Depending on those values could size update output incorrectly or produce truncated results for otherwise valid inputs. ## Validation - `./tools/format.js ext/node/polyfills/internal/crypto/cipher.ts tests/unit_node/crypto/crypto_cipher_test.ts` - `./tools/lint.js --js` - `cargo test -p unit_node_tests --test unit_node -- crypto::crypto_cipher_test`
What changed
ArrayBufferViewinputs using their underlying buffer, byte offset, and byte length.FastBufferconstructor.Buffer, multi-byte typed-array,DataView, and public allocator replacement cases.Why
Cipher updates consume bytes, while some typed-array properties describe element counts and public
Bufferallocation methods can be replaced by application code. Depending on those values could size update output incorrectly or produce truncated results for otherwise valid inputs.Validation
./tools/format.js ext/node/polyfills/internal/crypto/cipher.ts tests/unit_node/crypto/crypto_cipher_test.ts./tools/lint.js --jscargo test -p unit_node_tests --test unit_node -- crypto::crypto_cipher_testdprint check ext/node/polyfills/internal/crypto/cipher.ts tests/unit_node/crypto/crypto_cipher_test.tsnode tests/specs/node/crypto_cipheriv_spoofed_length/main.mjs | diff -u tests/specs/node/crypto_cipheriv_spoofed_length/main.out -