Skip to content

Commit 8ada774

Browse files
authored
fix: reject non-minimally encoded varints (#343)
Per the unsigned-varint spec, varints MUST be minimally encoded and MUST NOT exceed 9 bytes. `varint.decode` accepted non-minimal encodings such as `[0x81, 0x00]` for `1`, making CID and multihash prefixes malleable. Reject redundant final zero payload groups and varints longer than 9 bytes.
1 parent 6a65b8b commit 8ada774

3 files changed

Lines changed: 40 additions & 1 deletion

File tree

src/varint.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import varint from './vendor/varint.js'
22

33
export function decode (data: Uint8Array, offset = 0): [number, number] {
44
const code = varint.decode(data, offset)
5-
return [code, varint.decode.bytes]
5+
const length = varint.decode.bytes
6+
if (length > 9) {
7+
throw new RangeError('Invalid varint: too long')
8+
}
9+
if (length > 1 && data[offset + length - 1] === 0) {
10+
throw new RangeError('Invalid varint: not minimally encoded')
11+
}
12+
return [code, length]
613
}
714

815
export function encodeTo <T extends ArrayBufferLike> (int: number, target: Uint8Array<T>, offset = 0): Uint8Array<T> {

test/test-cid.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,14 @@ describe('CID', () => {
177177
assert.deepStrictEqual(cid.toString(), cidStr)
178178
})
179179

180+
it('rejects a non-minimally encoded varint prefix', () => {
181+
// the version varint 0x01 replaced by its non-minimal encoding 0x81 0x00
182+
const cidBuf = fromHex(
183+
'81007012207252523e6591fb8fe553d67ff55a86f84044b46a3e4176e10c58fa529a4aabd5'
184+
)
185+
assert.throws(() => CID.decode(cidBuf), /minimal/)
186+
})
187+
180188
it('create by parts', async () => {
181189
const hash = await sha256.digest(textEncoder.encode('abc'))
182190
const cid = CID.create(1, 0x71, hash)

test/test-varint.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,28 @@ describe('varint', () => {
2121
assert.deepStrictEqual(varint.decode(bytes), [outerTag, outerTagSize])
2222
assert.deepStrictEqual(varint.decode(bytes, outerTagSize), [innerTag, innerTagSize])
2323
})
24+
25+
it('rejects non-minimally encoded varints', () => {
26+
// 1 encodes minimally as [0x01]; [0x81, 0x00] is a non-minimal encoding of 1
27+
assert.throws(() => varint.decode(Uint8Array.from([0x81, 0x00])), /minimal/)
28+
// 0 encodes minimally as [0x00]; [0x80, 0x00] is a non-minimal encoding of 0
29+
assert.throws(() => varint.decode(Uint8Array.from([0x80, 0x00])), /minimal/)
30+
// non-minimal encoding at a non-zero offset
31+
assert.throws(() => varint.decode(Uint8Array.from([0xff, 0x81, 0x00]), 1), /minimal/)
32+
// 127 encodes minimally as [0x7f]; [0xff, 0x00] is a non-minimal encoding of 127
33+
assert.throws(() => varint.decode(Uint8Array.from([0xff, 0x00])), /minimal/)
34+
})
35+
36+
it('accepts interior zero groups', () => {
37+
// 16384 = 1 << 14; the two leading groups are zero but the encoding is minimal
38+
assert.deepStrictEqual(varint.decode(Uint8Array.from([0x80, 0x80, 0x01])), [16384, 3])
39+
})
40+
41+
it('rejects varints longer than 9 bytes', () => {
42+
// the unsigned-varint spec caps varints at 9 bytes (63 bits)
43+
assert.throws(
44+
() => varint.decode(Uint8Array.from([0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01])),
45+
/too long/
46+
)
47+
})
2448
})

0 commit comments

Comments
 (0)