security: add update size limits to prevent DOS attacks
This commit is contained in:
parent
4b865764b8
commit
d88422fc54
@ -45,6 +45,7 @@ import * as binary from 'lib0/binary'
|
||||
import * as map from 'lib0/map'
|
||||
import * as math from 'lib0/math'
|
||||
import * as array from 'lib0/array'
|
||||
import { assertMaxGCLength, assertMaxSkipLength, assertMaxStructs, assertMaxUpdates } from './limits.js'
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
@ -115,8 +116,10 @@ export const readClientsStructRefs = (decoder, doc) => {
|
||||
*/
|
||||
const clientRefs = map.create()
|
||||
const numOfStateUpdates = decoding.readVarUint(decoder.restDecoder)
|
||||
assertMaxUpdates(numOfStateUpdates)
|
||||
for (let i = 0; i < numOfStateUpdates; i++) {
|
||||
const numberOfStructs = decoding.readVarUint(decoder.restDecoder)
|
||||
assertMaxStructs(numberOfStructs)
|
||||
/**
|
||||
* @type {Array<GC|Item>}
|
||||
*/
|
||||
@ -130,6 +133,7 @@ export const readClientsStructRefs = (decoder, doc) => {
|
||||
switch (binary.BITS5 & info) {
|
||||
case 0: { // GC
|
||||
const len = decoder.readLen()
|
||||
assertMaxGCLength(len)
|
||||
refs[i] = new GC(createID(client, clock), len)
|
||||
clock += len
|
||||
break
|
||||
@ -137,6 +141,7 @@ export const readClientsStructRefs = (decoder, doc) => {
|
||||
case 10: { // Skip Struct (nothing to apply)
|
||||
// @todo we could reduce the amount of checks by adding Skip struct to clientRefs so we know that something is missing.
|
||||
const len = decoding.readVarUint(decoder.restDecoder)
|
||||
assertMaxSkipLength(len)
|
||||
refs[i] = new Skip(createID(client, clock), len)
|
||||
clock += len
|
||||
break
|
||||
|
48
src/utils/limits.js
Normal file
48
src/utils/limits.js
Normal file
@ -0,0 +1,48 @@
|
||||
export const MAX_STRUCTS = 100_000
|
||||
export const MAX_UPDATES = 100_000
|
||||
export const MAX_GC_LENGTH = 100_000
|
||||
export const MAX_SKIP_LENGTH = 100_000
|
||||
|
||||
/**
|
||||
* @param {number} numOfStateUpdates
|
||||
*/
|
||||
export function assertMaxUpdates (numOfStateUpdates) {
|
||||
if (numOfStateUpdates > MAX_UPDATES) {
|
||||
throw new Error(
|
||||
`This update exceeds the maximum number of updates. ${numOfStateUpdates} > ${MAX_UPDATES}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} numberOfStructs
|
||||
*/
|
||||
export function assertMaxStructs (numberOfStructs) {
|
||||
if (numberOfStructs > MAX_STRUCTS) {
|
||||
throw new Error(
|
||||
`This update exceeds the maximum number of structs. ${numberOfStructs} > ${MAX_STRUCTS}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} len
|
||||
*/
|
||||
export function assertMaxSkipLength (len) {
|
||||
if (len > MAX_SKIP_LENGTH) {
|
||||
throw new Error(
|
||||
`This skip length exceeds the limit. ${len} > ${MAX_SKIP_LENGTH}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} len
|
||||
*/
|
||||
export function assertMaxGCLength (len) {
|
||||
if (len > MAX_GC_LENGTH) {
|
||||
throw new Error(
|
||||
`This garbage collection update's length exceeds the limit. ${len} > ${MAX_GC_LENGTH}`
|
||||
)
|
||||
}
|
||||
}
|
@ -36,21 +36,25 @@ import {
|
||||
YXmlElement,
|
||||
YXmlHook
|
||||
} from '../internals.js'
|
||||
import { assertMaxGCLength, assertMaxSkipLength, assertMaxStructs, assertMaxUpdates } from './limits.js'
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
*/
|
||||
function * lazyStructReaderGenerator (decoder) {
|
||||
const numOfStateUpdates = decoding.readVarUint(decoder.restDecoder)
|
||||
assertMaxUpdates(numOfStateUpdates)
|
||||
for (let i = 0; i < numOfStateUpdates; i++) {
|
||||
const numberOfStructs = decoding.readVarUint(decoder.restDecoder)
|
||||
const client = decoder.readClient()
|
||||
let clock = decoding.readVarUint(decoder.restDecoder)
|
||||
assertMaxStructs(numberOfStructs)
|
||||
for (let i = 0; i < numberOfStructs; i++) {
|
||||
const info = decoder.readInfo()
|
||||
// @todo use switch instead of ifs
|
||||
if (info === 10) {
|
||||
const len = decoding.readVarUint(decoder.restDecoder)
|
||||
assertMaxSkipLength(len)
|
||||
yield new Skip(createID(client, clock), len)
|
||||
clock += len
|
||||
} else if ((binary.BITS5 & info) !== 0) {
|
||||
@ -74,6 +78,7 @@ function * lazyStructReaderGenerator (decoder) {
|
||||
clock += struct.length
|
||||
} else {
|
||||
const len = decoder.readLen()
|
||||
assertMaxGCLength(len)
|
||||
yield new GC(createID(client, clock), len)
|
||||
clock += len
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import * as doc from './doc.tests.js'
|
||||
import * as snapshot from './snapshot.tests.js'
|
||||
import * as updates from './updates.tests.js'
|
||||
import * as relativePositions from './relativePositions.tests.js'
|
||||
import * as limits from './limits.tests.js'
|
||||
|
||||
import { runTests } from 'lib0/testing'
|
||||
import { isBrowser, isNode } from 'lib0/environment'
|
||||
@ -25,7 +26,7 @@ if (isBrowser) {
|
||||
* @type {any}
|
||||
*/
|
||||
const tests = {
|
||||
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions
|
||||
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions, limits
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
|
93
tests/limits.tests.js
Normal file
93
tests/limits.tests.js
Normal file
@ -0,0 +1,93 @@
|
||||
import * as t from 'lib0/testing'
|
||||
import * as Y from '../src/index.js'
|
||||
|
||||
/**
|
||||
* @param {() => void} f function that should throw
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const shouldThrow = (f) => {
|
||||
try {
|
||||
f()
|
||||
return false
|
||||
} catch (/** @type {any} */ e) {
|
||||
console.log('Error thrown:', e?.message)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testShouldntMergeUpdatesWithTooManyStructs = (_tc) => {
|
||||
// Binary with 4398046511101 structs.
|
||||
const buf = new Uint8Array([
|
||||
0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 2, 0, 0, 22, 2, 0, 0, 1, 253, 255, 255,
|
||||
255, 255, 127, 0, 0
|
||||
])
|
||||
|
||||
t.assert(shouldThrow(() => {
|
||||
const update = Y.encodeStateAsUpdateV2(new Y.Doc())
|
||||
Y.mergeUpdatesV2([update, buf])
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testShouldntMergeUpdatesWithTooManyUpdatesV1 = (_tc) => {
|
||||
// Binary with 265828 updates.
|
||||
const buf = new Uint8Array([
|
||||
228, 156, 16, 0, 5, 255, 255, 5, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 237,
|
||||
0, 0, 0, 1, 1, 0, 254, 184, 194, 233, 173, 135, 217, 18, 0, 0, 1, 1,
|
||||
255, 237, 246
|
||||
])
|
||||
const update = Y.encodeStateAsUpdate(new Y.Doc())
|
||||
t.assert(shouldThrow(() => {
|
||||
Y.mergeUpdates([update, buf])
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testShouldntMergeUpdatesWithTooManyUpdatesV2 = (_tc) => {
|
||||
// Binary with 7658324286 updates.
|
||||
const buf = new Uint8Array([
|
||||
228, 149, 0, 0, 1, 24, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 237, 1, 190,
|
||||
130, 227, 195, 28, 1, 2, 228, 149, 0, 0, 1, 24, 0, 0, 1, 24, 0, 0,
|
||||
1, 0, 1, 237, 0
|
||||
])
|
||||
const update = Y.encodeStateAsUpdateV2(new Y.Doc())
|
||||
|
||||
t.assert(shouldThrow(() => {
|
||||
Y.mergeUpdatesV2([update, buf])
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testShouldntAcceptTooLargeGCItems = (_tc) => {
|
||||
// Update with a GC item of length 553253.
|
||||
const buf = new Uint8Array([
|
||||
143, 1, 128, 0, 0, 0, 1, 170, 1, 0, 1, 2, 0, 0, 22, 2, 0, 229, 196,
|
||||
67, 20, 231, 166, 139, 147, 174, 181, 253, 93, 232, 38, 154, 138,
|
||||
89, 0, 49, 213, 15, 18, 1, 48, 0, 0, 0
|
||||
])
|
||||
const update = Y.encodeStateAsUpdateV2(new Y.Doc())
|
||||
t.assert(shouldThrow(() => {
|
||||
Y.mergeUpdatesV2([update, buf])
|
||||
}))
|
||||
}
|
||||
|
||||
export const testShouldntApplyUpdatesOverLimit = () => {
|
||||
// Binary with 267854847 structs in one update.
|
||||
const buf = new Uint8Array([
|
||||
0, 1, 35, 0, 0, 0, 1, 2, 129, 0, 0, 16, 0, 199, 220, 0, 196, 122, 128,
|
||||
0, 65, 171, 234, 214, 0, 1, 0, 0, 1, 0, 0, 132, 0, 0, 16, 255, 199, 220,
|
||||
255, 0, 0, 0
|
||||
])
|
||||
t.assert(shouldThrow(() => {
|
||||
Y.applyUpdateV2(new Y.Doc(), buf)
|
||||
}))
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user