Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cbor-declared-length.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@evolution-sdk/evolution": patch
---

The CBOR decoder no longer reads past the end of its input or trusts a declared array or map length that the input cannot hold. The five bytes `9a7fffffff` declare an array of 2^31 - 1 items, and `CBOR.fromCBORHex` and `Transaction.fromCBORHex` allocated for them until the process ran out of memory. The map form `ba7fffffff` ran for tens of seconds. Both now throw `CBORError` at once, as other truncated input already did. Well-formed CBOR decodes as before.
5 changes: 5 additions & 0 deletions packages/evolution/src/CBOR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1904,6 +1904,7 @@ const bytesReadToByteSize = (bytesRead: number): ByteSize =>
bytesRead <= 1 ? 0 : bytesRead === 2 ? 1 : bytesRead === 3 ? 2 : 4

const decodeItemAt = (data: Uint8Array, offset: number, options: CodecOptions, track: DecodeTrack): DecodeAtResult => {
if (offset >= data.length) throw new CBORError({ message: "Insufficient data for CBOR item" })
const firstByte = data[offset]
const majorType = (firstByte >> 5) & 0x07
let result: DecodeAtResult
Expand Down Expand Up @@ -2143,6 +2144,8 @@ const decodeArrayAt = (data: Uint8Array, offset: number, options: CodecOptions,
} else {
const { bytesRead, length } = decodeLengthAt(data, offset)
let cur = offset + bytesRead
// Every item takes at least one byte
if (length > data.length - cur) throw new CBORError({ message: "Insufficient data for array" })
const arr: Array<CBOR> = new Array(length)
if (track === "none") {
for (let i = 0; i < length; i++) {
Expand Down Expand Up @@ -2197,6 +2200,8 @@ const decodeMapAt = (data: Uint8Array, offset: number, options: CodecOptions, tr
} else {
const { bytesRead, length } = decodeLengthAt(data, offset)
let cur = offset + bytesRead
// Every entry takes at least two bytes, one for the key and one for the value
if (length > (data.length - cur) / 2) throw new CBORError({ message: "Insufficient data for map" })
const map = isObj ? ({} as Record<string, CBOR>) : new Map<CBOR, CBOR>()
if (track === "none") {
for (let i = 0; i < length; i++) {
Expand Down
52 changes: 52 additions & 0 deletions packages/evolution/test/CBOR.DecodeLength.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, it } from "@effect/vitest"

import * as CBOR from "../src/CBOR.js"
import * as Transaction from "../src/Transaction.js"

// A definite array or map header declares how many items follow. The decoder
// must not read past the end of the input or trust a length the input cannot hold.

const decoders = [
["CBOR.fromCBORHex", (hex: string) => CBOR.fromCBORHex(hex)],
["CBOR.fromCBORHexWithFormat", (hex: string) => CBOR.fromCBORHexWithFormat(hex)],
["Transaction.fromCBORHex", (hex: string) => Transaction.fromCBORHex(hex)]
] as const

const decodeError = (decode: (hex: string) => unknown, hex: string): unknown => {
try {
decode(hex)
} catch (error) {
return error
}
return undefined
}

describe("CBOR decoding of declared lengths", () => {
const cases = [
{ name: "an array declaring 2^31 - 1 items with none present", hex: "9a7fffffff", message: "Insufficient data for array" },
{ name: "a map declaring 2^31 - 1 entries with none present", hex: "ba7fffffff", message: "Insufficient data for map" },
{ name: "an array missing its last item", hex: "8201", message: "Insufficient data for array" },
{ name: "a map missing its value", hex: "a101", message: "Insufficient data for map" },
{ name: "a tag with no content", hex: "d818", message: "Insufficient data for CBOR item" }
]

for (const [label, decode] of decoders) {
it.each(cases)(`${label} throws CBORError for $name`, ({ hex, message }) => {
const error = decodeError(decode, hex)
expect(error).toBeInstanceOf(CBOR.CBORError)
expect((error as CBOR.CBORError).message).toContain(message)
})
}

it("decodes arrays and maps whose items fill the input exactly", () => {
expect(CBOR.fromCBORHex("83010203")).toEqual([1n, 2n, 3n])
expect(CBOR.fromCBORHex("a201020304")).toEqual(
new Map([
[1n, 2n],
[3n, 4n]
])
)
expect(CBOR.fromCBORHex("8180")).toEqual([[]])
expect(CBOR.fromCBORHex("9f0102ff")).toEqual([1n, 2n])
})
})
Loading