Skip to content

Commit 0eb493c

Browse files
committed
[decoding] add limits to variable-length decoders
This commit allows decoder users to limit how many characters or array elements they want to decode. Such limits are useful in a server environment because `lib0` synchronously decodes messages. An attacker just send a message with a very long string or array and stall the main thread of the server so that no other requests can be served. The length limits have been added to these public functions: - `readVarUint8Array` - `readTerminatedUint8Array` - `readVarString` - `peekVarString` - `readTerminatedString` - The constructor of `StringDecoder` Each of these functions accepts a new trailing optional argument `maxLen`. The change should therefore not be breaking. I have added some tests, the code is fully covered.
1 parent b6d73ac commit 0eb493c

File tree

2 files changed

+85
-10
lines changed

2 files changed

+85
-10
lines changed

decoding.js

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import * as encoding from './encoding.js'
3535

3636
const errorUnexpectedEndOfArray = error.create('Unexpected end of array')
3737
const errorIntegerOutOfRange = error.create('Integer out of Range')
38+
const errorValueTooLong = error.create('Value too long')
3839

3940
/**
4041
* A Decoder handles the decoding of an Uint8Array.
@@ -113,9 +114,17 @@ export const readUint8Array = (decoder, len) => {
113114
*
114115
* @function
115116
* @param {Decoder} decoder
117+
* @param {number} [maxLen] Maximum length of the array
116118
* @return {Uint8Array}
117119
*/
118-
export const readVarUint8Array = decoder => readUint8Array(decoder, readVarUint(decoder))
120+
export const readVarUint8Array = (decoder, maxLen) => {
121+
const len = readVarUint(decoder)
122+
if (maxLen !== undefined && len > maxLen) {
123+
throw errorValueTooLong
124+
}
125+
126+
return readUint8Array(decoder, len)
127+
}
119128

120129
/**
121130
* Read the rest of the content as an ArrayBuffer
@@ -336,13 +345,16 @@ export const peekVarInt = decoder => {
336345
*
337346
* @function
338347
* @param {Decoder} decoder
348+
* @param {number} [maxLen] Maximum length of the string in bytes
339349
* @return {String} The read String.
340350
*/
341351
/* c8 ignore start */
342-
export const _readVarStringPolyfill = decoder => {
352+
export const _readVarStringPolyfill = (decoder, maxLen) => {
343353
let remainingLen = readVarUint(decoder)
344354
if (remainingLen === 0) {
345355
return ''
356+
} else if (maxLen !== undefined && remainingLen > maxLen) {
357+
throw errorValueTooLong
346358
} else {
347359
let encodedString = String.fromCodePoint(readUint8(decoder)) // remember to decrease remainingLen
348360
if (--remainingLen < 100) { // do not create a Uint8Array for small strings
@@ -368,17 +380,19 @@ export const _readVarStringPolyfill = decoder => {
368380
/**
369381
* @function
370382
* @param {Decoder} decoder
383+
* @param {number} [maxLen] Maximum length of the string in bytes
371384
* @return {String} The read String
372385
*/
373-
export const _readVarStringNative = decoder =>
374-
/** @type any */ (string.utf8TextDecoder).decode(readVarUint8Array(decoder))
386+
export const _readVarStringNative = (decoder, maxLen) =>
387+
/** @type any */ (string.utf8TextDecoder).decode(readVarUint8Array(decoder, maxLen))
375388

376389
/**
377390
* Read string of variable length
378391
* * varUint is used to store the length of the string
379392
*
380393
* @function
381394
* @param {Decoder} decoder
395+
* @param {number} [maxLen] Maximum length of the string in bytes
382396
* @return {String} The read String
383397
*
384398
*/
@@ -387,12 +401,16 @@ export const readVarString = string.utf8TextDecoder ? _readVarStringNative : _re
387401

388402
/**
389403
* @param {Decoder} decoder
404+
* @param {number} [maxLen] Maximum length of the array
390405
* @return {Uint8Array}
391406
*/
392-
export const readTerminatedUint8Array = decoder => {
407+
export const readTerminatedUint8Array = (decoder, maxLen) => {
393408
const encoder = encoding.createEncoder()
394409
let b
395410
while (true) {
411+
if (maxLen !== undefined && encoding.length(encoder) > maxLen) {
412+
throw errorValueTooLong
413+
}
396414
b = readUint8(decoder)
397415
if (b === 0) {
398416
return encoding.toUint8Array(encoder)
@@ -406,20 +424,22 @@ export const readTerminatedUint8Array = decoder => {
406424

407425
/**
408426
* @param {Decoder} decoder
427+
* @param {number} [maxLen] Maximum length of the array string in bytes
409428
* @return {string}
410429
*/
411-
export const readTerminatedString = decoder => string.decodeUtf8(readTerminatedUint8Array(decoder))
430+
export const readTerminatedString = (decoder, maxLen) => string.decodeUtf8(readTerminatedUint8Array(decoder, maxLen))
412431

413432
/**
414433
* Look ahead and read varString without incrementing position
415434
*
416435
* @function
417436
* @param {Decoder} decoder
437+
* @param {number} [maxLen] Maximum length of the array string in bytes
418438
* @return {string}
419439
*/
420-
export const peekVarString = decoder => {
440+
export const peekVarString = (decoder, maxLen) => {
421441
const pos = decoder.pos
422-
const s = readVarString(decoder)
442+
const s = readVarString(decoder, maxLen)
423443
decoder.pos = pos
424444
return s
425445
}
@@ -684,10 +704,11 @@ export class IntDiffOptRleDecoder extends Decoder {
684704
export class StringDecoder {
685705
/**
686706
* @param {Uint8Array} uint8Array
707+
* @param {number} [maxLen] Maximum length of the string in bytes
687708
*/
688-
constructor (uint8Array) {
709+
constructor (uint8Array, maxLen) {
689710
this.decoder = new UintOptRleDecoder(uint8Array)
690-
this.str = readVarString(this.decoder)
711+
this.str = readVarString(this.decoder, maxLen)
691712
/**
692713
* @type {number}
693714
*/

encoding.test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,22 @@ export const testStringDecoder = tc => {
802802
}
803803
}
804804

805+
/**
806+
* @param {t.TestCase} _tc
807+
*/
808+
export const testStringDecoderLengthLimit = _tc => {
809+
const tooLongText = 'a'.repeat(100)
810+
const encoderA = new encoding.StringEncoder()
811+
encoderA.write(tooLongText)
812+
t.fails(() => new decoding.StringDecoder(encoderA.toUint8Array(), 10))
813+
814+
const okayText = 'a'.repeat(9)
815+
const encoderB = new encoding.StringEncoder()
816+
encoderB.write(okayText)
817+
const decoder = new decoding.StringDecoder(encoderB.toUint8Array(), 10)
818+
t.assert(decoder.read() === okayText)
819+
}
820+
805821
/**
806822
* @param {t.TestCase} tc
807823
*/
@@ -869,3 +885,41 @@ export const testTerminatedEncodering = _tc => {
869885
t.compare(readBuf1, buf1)
870886
t.compare(readBuf2, buf2)
871887
}
888+
889+
/**
890+
* @param {t.TestCase} _tc
891+
*/
892+
export const testVarUint8ArrayLengthLimitEncoding = _tc => {
893+
const buf1 = new Uint8Array([0, 1, 2, 255, 4, 5])
894+
const buf2 = new Uint8Array([255, 255, 0, 0, 0, 1, 0, 0])
895+
896+
const encoder = encoding.createEncoder()
897+
encoding.writeVarUint8Array(encoder, buf1)
898+
encoding.writeVarUint8Array(encoder, buf2)
899+
900+
const decoder = decoding.createDecoder(encoding.toUint8Array(encoder))
901+
const readBuf1 = decoding.readVarUint8Array(decoder, 6)
902+
t.compare(readBuf1, buf1)
903+
t.fails(() => {
904+
decoding.readVarUint8Array(decoder, 6)
905+
})
906+
}
907+
908+
/**
909+
* @param {t.TestCase} _tc
910+
*/
911+
export const testTerminatedUint8ArrayLengthLimitEncoding = _tc => {
912+
const buf1 = new Uint8Array([0, 1, 2, 255, 4, 5])
913+
const buf2 = new Uint8Array([255, 255, 0, 0, 0, 1, 0, 0])
914+
915+
const encoder = encoding.createEncoder()
916+
encoding.writeTerminatedUint8Array(encoder, buf1)
917+
encoding.writeTerminatedUint8Array(encoder, buf2)
918+
919+
const decoder = decoding.createDecoder(encoding.toUint8Array(encoder))
920+
const readBuf1 = decoding.readTerminatedUint8Array(decoder, 6)
921+
t.compare(readBuf1, buf1)
922+
t.fails(() => {
923+
decoding.readTerminatedUint8Array(decoder, 6)
924+
})
925+
}

0 commit comments

Comments
 (0)