Skip to content

fix(node_crypto): size cipher updates by byte length - #36459

Merged
nathanwhit merged 3 commits into
denoland:mainfrom
nathanwhit:fix/node-crypto-cipher-byte-lengths
Aug 17, 2026
Merged

nathanwhit merged 3 commits into
denoland:mainfrom
nathanwhit:fix/node-crypto-cipher-byte-lengths

Conversation

@nathanwhit

@nathanwhit nathanwhit commented Aug 6, 2026 •

Copy link
Copy Markdown
Member

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.
  • Align the inherited spoofed-length spec with byte-oriented output sizing.

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
  • dprint check ext/node/polyfills/internal/crypto/cipher.ts tests/unit_node/crypto/crypto_cipher_test.ts
  • node tests/specs/node/crypto_cipheriv_spoofed_length/main.mjs | diff -u tests/specs/node/crypto_cipheriv_spoofed_length/main.out -

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 byteLength via the primordial getters, so element-count views are correct and a spoofed .length own-property can't influence it;
  • toFastBufferView normalizing the view so #[buffer] input: &[u8] sees the right bytes for a DataView or an offset view rather than whatever byteOffset-less coercion happened before;
  • checking the op's boolean and throwing instead of returning the buffer, plus new FastBuffer(n) (zero-filled) rather than allocUnsafe, 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.

@nathanwhit
nathanwhit enabled auto-merge (squash) August 17, 2026 18:50
@nathanwhit
nathanwhit merged commit e518437 into denoland:main Aug 17, 2026
137 checks passed
bartlomieju pushed a commit that referenced this pull request Aug 27, 2026
## 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`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants