diff --git a/javascript/packages/fory/lib/fory.ts b/javascript/packages/fory/lib/fory.ts index d26b6b6fa7..c0f4cf984b 100644 --- a/javascript/packages/fory/lib/fory.ts +++ b/javascript/packages/fory/lib/fory.ts @@ -55,10 +55,14 @@ export default class { private initConfig(config: Partial | undefined) { return { + hps: config?.hps, refTracking: config?.refTracking !== null ? Boolean(config?.refTracking) : null, useSliceString: Boolean(config?.useSliceString), hooks: config?.hooks || {}, compatible: Boolean(config?.compatible), + maxBinarySize: config?.maxBinarySize, + maxCollectionSize: config?.maxCollectionSize, + maxMapEntries: config?.maxMapEntries, }; } diff --git a/javascript/packages/fory/lib/gen/collection.ts b/javascript/packages/fory/lib/gen/collection.ts index 9278b6dcc0..c34502c58a 100644 --- a/javascript/packages/fory/lib/gen/collection.ts +++ b/javascript/packages/fory/lib/gen/collection.ts @@ -145,6 +145,10 @@ class CollectionAnySerializer { read(accessor: (result: any, index: number, v: any) => void, createCollection: (len: number) => any, fromRef: boolean): any { void fromRef; const len = this.fory.binaryReader.readVarUint32Small7(); + const maxSize = this.fory.config.maxCollectionSize; + if (typeof maxSize === "number" && maxSize > 0 && len > maxSize) { + throw new Error(`Collection length ${len} exceeds configured maxCollectionSize ${maxSize}`); + } const flags = this.fory.binaryReader.readUint8(); const isSame = flags & CollectionFlags.SAME_TYPE; const includeNone = flags & CollectionFlags.HAS_NULL; @@ -295,8 +299,17 @@ export abstract class CollectionSerializerGenerator extends BaseSerializerGenera const flags = this.scope.uniqueName("flags"); const idx = this.scope.uniqueName("idx"); const refFlag = this.scope.uniqueName("refFlag"); + const maxSizeConfig = this.builder.fory.config.maxCollectionSize; + const limitCheck = typeof maxSizeConfig === "number" && maxSizeConfig > 0 + ? ` + if (${len} > ${maxSizeConfig}) { + throw new Error(\`Collection length \${${len}} exceeds configured maxCollectionSize ${maxSizeConfig}\`); + } + ` + : ""; return ` const ${len} = ${this.builder.reader.readVarUint32Small7()}; + ${limitCheck} const ${flags} = ${this.builder.reader.readUint8()}; const ${result} = ${this.newCollection(len)}; ${this.maybeReference(result, refState)} diff --git a/javascript/packages/fory/lib/gen/map.ts b/javascript/packages/fory/lib/gen/map.ts index d94f05606a..c59baa6332 100644 --- a/javascript/packages/fory/lib/gen/map.ts +++ b/javascript/packages/fory/lib/gen/map.ts @@ -235,6 +235,10 @@ class MapAnySerializer { read(fromRef: boolean): any { let count = this.fory.binaryReader.readVarUint32Small7(); + const maxEntries = this.fory.config.maxMapEntries; + if (typeof maxEntries === "number" && maxEntries > 0 && count > maxEntries) { + throw new Error(`Map entry count ${count} exceeds configured maxMapEntries ${maxEntries}`); + } const result = new Map(); if (fromRef) { this.fory.referenceResolver.reference(result); @@ -400,8 +404,17 @@ export class MapSerializerGenerator extends BaseSerializerGenerator { const count = this.scope.uniqueName("count"); const result = this.scope.uniqueName("result"); + const maxEntriesConfig = this.builder.fory.config.maxMapEntries; + const limitCheck = typeof maxEntriesConfig === "number" && maxEntriesConfig > 0 + ? ` + if (${count} > ${maxEntriesConfig}) { + throw new Error(\`Map entry count \${${count}} exceeds configured maxMapEntries ${maxEntriesConfig}\`); + } + ` + : ""; return ` let ${count} = ${this.builder.reader.readVarUint32Small7()}; + ${limitCheck} const ${result} = new Map(); if (${refState}) { ${this.builder.referenceResolver.reference(result)} diff --git a/javascript/packages/fory/lib/reader/index.ts b/javascript/packages/fory/lib/reader/index.ts index 4300bc9c52..47ef342ffd 100644 --- a/javascript/packages/fory/lib/reader/index.ts +++ b/javascript/packages/fory/lib/reader/index.ts @@ -30,11 +30,14 @@ export class BinaryReader { private platformBuffer!: PlatformBuffer; private bigString = ""; private byteLength = 0; + private maxBinarySize?: number; constructor(config: { useSliceString?: boolean; + maxBinarySize?: number; }) { this.sliceStringEnable = isNodeEnv && config.useSliceString; + this.maxBinarySize = config.maxBinarySize; } reset(ab: Uint8Array) { @@ -189,6 +192,9 @@ export class BinaryReader { const header = this.readVarUint36Small(); const type = header & 0b11; const len = header >>> 2; + if (typeof this.maxBinarySize === "number" && this.maxBinarySize > 0 && len > this.maxBinarySize) { + throw new Error(`String byte length ${len} exceeds configured maxBinarySize ${this.maxBinarySize}`); + } switch (type) { case LATIN1: return this.stringLatin1(len); diff --git a/javascript/packages/fory/lib/type.ts b/javascript/packages/fory/lib/type.ts index 983c4797ce..ea62754404 100644 --- a/javascript/packages/fory/lib/type.ts +++ b/javascript/packages/fory/lib/type.ts @@ -268,6 +268,9 @@ export interface Config { afterCodeGenerated?: (code: string) => string; }; compatible?: boolean; + maxBinarySize?: number; + maxCollectionSize?: number; + maxMapEntries?: number; } export interface WithForyClsInfo { diff --git a/javascript/test/array.test.ts b/javascript/test/array.test.ts index 0abbd571a7..e96578e39c 100644 --- a/javascript/test/array.test.ts +++ b/javascript/test/array.test.ts @@ -148,6 +148,22 @@ describe('array', () => { expect(result.a7[1].toFloat32()).toBeCloseTo(-2.5, 2); expect(result.a7[2].toFloat32()).toBe(0); }); + + test('should enforce maxCollectionSize for list field', () => { + const typeinfo = Type.struct({ + typeName: "example.limit" + }, { + items: Type.array(Type.string()), + }); + const fory = new Fory({ refTracking: true, maxCollectionSize: 2 }); + const { serialize, deserialize } = fory.registerSerializer(typeinfo); + + const okBin = serialize({ items: ["a", "b"] }); + expect(deserialize(okBin)).toEqual({ items: ["a", "b"] }); + + const tooManyBin = serialize({ items: ["a", "b", "c"] }); + expect(() => deserialize(tooManyBin)).toThrow(/maxCollectionSize/); + }); }); diff --git a/javascript/test/map.test.ts b/javascript/test/map.test.ts index 0dae5ce947..64d574b922 100644 --- a/javascript/test/map.test.ts +++ b/javascript/test/map.test.ts @@ -43,6 +43,16 @@ describe('map', () => { const result = deserialize(bin); expect(result).toEqual({ f1: new Map([["hello", 123],["world", 456]])}) }); + + test('should enforce maxMapEntries when configured', () => { + const fory = new Fory({ refTracking: true, maxMapEntries: 1 }); + + const okInput = fory.serialize(new Map([["foo", "bar"]])); + expect(fory.deserialize(okInput)).toEqual(new Map([["foo", "bar"]])); + + const tooManyInput = fory.serialize(new Map([["foo", "bar"], ["baz", "qux"]])); + expect(() => fory.deserialize(tooManyInput)).toThrow(/maxMapEntries/); + }); }); diff --git a/javascript/test/string.test.ts b/javascript/test/string.test.ts index 1bcfe951f9..500f5bffa6 100644 --- a/javascript/test/string.test.ts +++ b/javascript/test/string.test.ts @@ -62,4 +62,16 @@ describe('string', () => { ); expect(result).toEqual(str) }); + + test('should enforce maxBinarySize when configured', () => { + const short = "abcd"; + const long = "abcdef"; + const fory = new Fory({ ...config, maxBinarySize: 4 }); + + const ok = fory.serialize(short); + expect(fory.deserialize(ok)).toEqual(short); + + const tooLong = fory.serialize(long); + expect(() => fory.deserialize(tooLong)).toThrow(/maxBinarySize/); + }); });