Skip to content

fix(cbor): reject truncated and length-lying input instead of reading past the end - #664

Closed
Kshot3000 wants to merge 1 commit into
IntersectMBO:mainfrom
Kshot3000:fix/cbor-decoder-bounds-661
Closed

Kshot3000 wants to merge 1 commit into
IntersectMBO:mainfrom
Kshot3000:fix/cbor-decoder-bounds-661

Conversation

@Kshot3000

Copy link
Copy Markdown

Summary

Fixes #661. The CBOR decoder trusted declared lengths and read past the end of its input:

  • CBOR.fromCBORHex("9a7fffffff") — five bytes declaring an array of 2^31 − 1 items — aborted the Node process with "JavaScript heap out of memory" (exit 134), which try/catch cannot stop. Reproduced locally before the fix; after the fix it throws CBORError in ~1 ms.
  • Transaction.fromCBORHex hit the same path.

Cause

In packages/evolution/src/CBOR.ts:

  • decodeItemAt reads data[offset] with no bounds check. Past the end that is undefined, which bit-shifts to major type 0, so a truncated input silently decodes items as the integer 0 instead of failing.
  • decodeArrayAt allocates new Array(length) for the declared length before decoding anything, and decodeMapAt loops over it.
  • decodeLengthAt builds a 4-byte length with signed 32-bit shifts, so a length with the top bit set (e.g. 0xffffffff) reads as a negative number and slips past both loops.

Fix

Three checks, all of which well-formed CBOR never hits (so data read from the chain decodes exactly as before):

  1. decodeItemAt throws CBORError when the offset is at or past the end of the input.
  2. A definite array whose declared length exceeds the bytes left throws before allocating (every item takes at least one byte); a definite map whose declared pair count needs more than the bytes left throws the same way (every key and value takes at least one byte).
  3. The 4-byte length in decodeLengthAt is read unsigned (>>> 0).

Tests

New CBOR Decoder Bounds (issue #661) block in test/CBOR.test.ts:

  • 9a7fffffff and ba7fffffff throw CBORError (array and map forms, including through fromCBORHexWithFormat).
  • baffffffff throws — the signed-length case.
  • 8300, 81 and a100 throw — declared items/pairs running past the end.
  • c0 and 81c0 throw — an item starting exactly at the end of the input.
  • Boundary controls still decode: 83010203, a10102, and a 24-item array announced with a 1-byte length header.

Full package suite: 1697 passed, 75 skipped, 0 test failures. Three test files fail to import in this checkout for pre-existing environment reasons, untouched by this PR: test/CBOR.KeyOrder.test.ts (FastCheck from effect — verified broken the same way on unmodified main), and test/Transaction.CanonicalWitnessAdd.test.ts / test/TxBuilder.CodecOptions.test.ts (cardano-hw-interop-lib is not installed in the checkout).

A patch changeset is included.


Building in the open as an independent Cardano developer — not affiliated with the Cardano Foundation, IOG or Intersect.

@Kshot9000 · https://x.com/kshot9000 · github.com/Kshot3000
ADA donations: addr1q8hnl6vl5a6k3rw3n5g3jtte696zcl76kfatzv7gpswa9r0dj7fma6klq55y4ffm7tf0em09udnyhuk4ah92pl5x9jpqjae44v

cc @cardano-foundation @IntersectMBO

… past the end

decodeItemAt read data[offset] unchecked, so an offset past the end
decoded undefined as the integer 0. decodeArrayAt allocated
new Array(length) for the declared length first, so the five bytes
9a7fffffff (array of 2^31 - 1 items, none present) aborted the Node
process with a heap out-of-memory that try/catch cannot stop, and
decodeMapAt looped over the declared count. A 4-byte length also read
as a signed number, so 0xffffffff became -1 and slipped past the loops.

- decodeItemAt throws CBORError at or past the end of the input
- definite array/map lengths larger than the bytes left throw before
  allocating or looping (each item, key and value takes >= 1 byte)
- decodeLengthAt reads the 4-byte length unsigned (>>> 0)

Well-formed CBOR never hits any of these checks. Fixes IntersectMBO#661.
@Kshot3000

Copy link
Copy Markdown
Author

Closing this in favour of #663 — @solidsnakedev opened it 35 minutes before this PR, fixing the same issue with the same bounds checks, and one fix for #661 is enough for the maintainers to review.

The one difference in this version — reading the 4-byte length in decodeLengthAt unsigned, so a declared length with the top bit set can't slip past as a negative number — is verified and left as a comment on #663 instead (baffffffff currently returns an empty map on that branch; 9affffffff throws a raw RangeError), so whichever version lands can pick it up. Happy to reopen if you'd rather take this one.


Building in the open as an independent Cardano developer — not affiliated with the Cardano Foundation, IOG or Intersect.

@Kshot9000 · https://x.com/kshot9000 · github.com/Kshot3000
ADA donations: addr1q8hnl6vl5a6k3rw3n5g3jtte696zcl76kfatzv7gpswa9r0dj7fma6klq55y4ffm7tf0em09udnyhuk4ah92pl5x9jpqjae44v

cc @cardano-foundation @IntersectMBO

@Kshot3000 Kshot3000 closed this Oct 16, 2026
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.

CBOR decoder reads past the end of its input and trusts declared lengths

1 participant