diff --git a/dart/packages/fory-test/test/config_test/size_guard_test.dart b/dart/packages/fory-test/test/config_test/size_guard_test.dart new file mode 100644 index 0000000000..a52ed2b562 --- /dev/null +++ b/dart/packages/fory-test/test/config_test/size_guard_test.dart @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +library; + +import 'dart:typed_data'; +import 'package:fory/fory.dart'; +import 'package:fory/src/exception/fory_exception.dart'; +import 'package:test/test.dart'; + +void main() { + group('maxCollectionSize guard check', () { + test('list within limit deserializes successfully', () { + final foryWrite = Fory(); + final bytes = foryWrite.serialize([1, 2, 3]); + + final foryRead = Fory(maxCollectionSize: 10); + final result = foryRead.deserialize(bytes); + expect(result, equals([1, 2, 3])); + }); + + test('list exceeding limit throws InvalidDataException', () { + final foryWrite = Fory(); + final bytes = foryWrite.serialize([1, 2, 3, 4, 5]); + + final foryRead = Fory(maxCollectionSize: 3); + expect( + () => foryRead.deserialize(bytes), + throwsA(isA()), + ); + }); + + test('list at exact limit deserializes successfully', () { + final foryWrite = Fory(); + final bytes = foryWrite.serialize([1, 2, 3]); + + final foryRead = Fory(maxCollectionSize: 3); + final result = foryRead.deserialize(bytes); + expect(result, equals([1, 2, 3])); + }); + + test('empty list always deserializes successfully', () { + final foryWrite = Fory(); + final bytes = foryWrite.serialize([]); + + final foryRead = Fory(maxCollectionSize: 0); + final result = foryRead.deserialize(bytes); + expect(result, equals([])); + }); + + test('map exceeding limit throws InvalidDataException', () { + final foryWrite = Fory(); + final bytes = foryWrite.serialize({'a': 1, 'b': 2, 'c': 3}); + + final foryRead = Fory(maxCollectionSize: 2); + expect( + () => foryRead.deserialize(bytes), + throwsA(isA()), + ); + }); + + test('map within limit deserializes successfully', () { + final foryWrite = Fory(); + final bytes = foryWrite.serialize({'a': 1, 'b': 2}); + + final foryRead = Fory(maxCollectionSize: 10); + final result = foryRead.deserialize(bytes); + expect(result, equals({'a': 1, 'b': 2})); + }); + + test('set exceeding limit throws InvalidDataException', () { + final foryWrite = Fory(); + final bytes = foryWrite.serialize({1, 2, 3, 4, 5}); + + final foryRead = Fory(maxCollectionSize: 2); + expect( + () => foryRead.deserialize(bytes), + throwsA(isA()), + ); + }); + + test('set within limit deserializes successfully', () { + final foryWrite = Fory(); + final bytes = foryWrite.serialize({1, 2, 3}); + + final foryRead = Fory(maxCollectionSize: 10); + final result = foryRead.deserialize(bytes); + expect(result, equals({1, 2, 3})); + }); + + test('default maxCollectionSize allows normal sizes', () { + final foryWrite = Fory(); + final largeList = List.generate(1000, (i) => i); + final bytes = foryWrite.serialize(largeList); + + final foryRead = Fory(); + final result = foryRead.deserialize(bytes) as List; + expect(result.length, 1000); + }); + }); + + group('maxBinarySize guard check', () { + test('binary within limit deserializes successfully', () { + final foryWrite = Fory(); + final data = Uint8List.fromList([1, 2, 3, 4, 5]); + final bytes = foryWrite.serialize(data); + + final foryRead = Fory(maxBinarySize: 10); + final result = foryRead.deserialize(bytes) as Uint8List; + expect(result, equals(data)); + }); + + test('binary exceeding limit throws InvalidDataException', () { + final foryWrite = Fory(); + final data = Uint8List.fromList([1, 2, 3, 4, 5]); + final bytes = foryWrite.serialize(data); + + final foryRead = Fory(maxBinarySize: 3); + expect( + () => foryRead.deserialize(bytes), + throwsA(isA()), + ); + }); + + test('binary at exact limit deserializes successfully', () { + final foryWrite = Fory(); + final data = Uint8List.fromList([1, 2, 3]); + final bytes = foryWrite.serialize(data); + + final foryRead = Fory(maxBinarySize: 3); + final result = foryRead.deserialize(bytes) as Uint8List; + expect(result, equals(data)); + }); + + test('empty binary always deserializes successfully', () { + final foryWrite = Fory(); + final data = Uint8List(0); + final bytes = foryWrite.serialize(data); + + final foryRead = Fory(maxBinarySize: 0); + final result = foryRead.deserialize(bytes) as Uint8List; + expect(result, equals(data)); + }); + + test('default maxBinarySize allows normal sizes', () { + final foryWrite = Fory(); + final data = Uint8List.fromList(List.generate(1000, (i) => i % 256)); + final bytes = foryWrite.serialize(data); + + final foryRead = Fory(); + final result = foryRead.deserialize(bytes) as Uint8List; + expect(result.length, 1000); + }); + }); + + group('combined guard check', () { + test('both limits enforced independently', () { + final foryWrite = Fory(); + + final listBytes = foryWrite.serialize([1, 2, 3, 4, 5]); + final binaryBytes = + foryWrite.serialize(Uint8List.fromList([1, 2, 3, 4, 5])); + + final foryRead = Fory(maxCollectionSize: 3, maxBinarySize: 10); + + // Collection exceeds limit + expect( + () => foryRead.deserialize(listBytes), + throwsA(isA()), + ); + + // Binary within limit + final result = foryRead.deserialize(binaryBytes) as Uint8List; + expect(result.length, 5); + }); + + test('default values are applied', () { + final config = ForyConfig(); + expect(config.maxCollectionSize, ForyConfig.defaultMaxCollectionSize); + expect(config.maxBinarySize, ForyConfig.defaultMaxBinarySize); + }); + }); +} diff --git a/dart/packages/fory/lib/src/config/fory_config.dart b/dart/packages/fory/lib/src/config/fory_config.dart index 73c3b6f128..8c92ae34f4 100644 --- a/dart/packages/fory/lib/src/config/fory_config.dart +++ b/dart/packages/fory/lib/src/config/fory_config.dart @@ -23,6 +23,11 @@ final class ForyConfig { final bool basicTypesRefIgnored; final bool timeRefIgnored; final bool stringRefIgnored; + final int maxBinarySize; + final int maxCollectionSize; + + static const int defaultMaxBinarySize = 64 * 1024 * 1024; + static const int defaultMaxCollectionSize = 1000000; const ForyConfig({ this.compatible = false, @@ -30,5 +35,7 @@ final class ForyConfig { this.basicTypesRefIgnored = true, this.timeRefIgnored = true, this.stringRefIgnored = false, + this.maxBinarySize = defaultMaxBinarySize, + this.maxCollectionSize = defaultMaxCollectionSize, }); } diff --git a/dart/packages/fory/lib/src/deserialization_context.dart b/dart/packages/fory/lib/src/deserialization_context.dart index c985601538..764a187316 100644 --- a/dart/packages/fory/lib/src/deserialization_context.dart +++ b/dart/packages/fory/lib/src/deserialization_context.dart @@ -17,6 +17,7 @@ * under the License. */ +import 'package:fory/src/config/fory_config.dart'; import 'package:fory/src/deserialization_dispatcher.dart'; import 'package:fory/src/meta/spec_wraps/type_spec_wrap.dart'; import 'package:fory/src/resolver/deserialization_ref_resolver.dart'; @@ -26,6 +27,7 @@ import 'package:fory/src/runtime_context.dart'; import 'package:fory/src/collection/stack.dart'; final class DeserializationContext extends Pack { + final ForyConfig config; final HeaderBrief header; final DeserializationDispatcher deserializationDispatcher; @@ -38,6 +40,7 @@ final class DeserializationContext extends Pack { const DeserializationContext( super.structHashResolver, super.getTagByDartType, + this.config, this.header, this.deserializationDispatcher, this.refResolver, diff --git a/dart/packages/fory/lib/src/deserialization_dispatcher.dart b/dart/packages/fory/lib/src/deserialization_dispatcher.dart index ede6f8d9cc..c9e38febdc 100644 --- a/dart/packages/fory/lib/src/deserialization_dispatcher.dart +++ b/dart/packages/fory/lib/src/deserialization_dispatcher.dart @@ -58,6 +58,7 @@ class DeserializationDispatcher { DeserializationContext deserializationContext = DeserializationContext( StructHashResolver.inst, typeResolver.getRegisteredTag, + conf, header, this, DeserializationRefResolver.getOne(conf.ref), diff --git a/dart/packages/fory/lib/src/exception/fory_exception.dart b/dart/packages/fory/lib/src/exception/fory_exception.dart index 3d0dafcc9d..71f20e0ac8 100644 --- a/dart/packages/fory/lib/src/exception/fory_exception.dart +++ b/dart/packages/fory/lib/src/exception/fory_exception.dart @@ -29,3 +29,14 @@ abstract class ForyException extends Error { return buf.toString(); } } + +class InvalidDataException extends ForyException { + final String message; + + InvalidDataException(this.message); + + @override + void giveExceptionMessage(StringBuffer buf) { + buf.write(message); + } +} diff --git a/dart/packages/fory/lib/src/fory_impl.dart b/dart/packages/fory/lib/src/fory_impl.dart index d2298ad464..141d910761 100644 --- a/dart/packages/fory/lib/src/fory_impl.dart +++ b/dart/packages/fory/lib/src/fory_impl.dart @@ -42,6 +42,8 @@ final class Fory { bool basicTypesRefIgnored = true, bool timeRefIgnored = true, bool stringRefIgnored = false, + int maxBinarySize = ForyConfig.defaultMaxBinarySize, + int maxCollectionSize = ForyConfig.defaultMaxCollectionSize, }) : this.fromConfig( ForyConfig( compatible: compatible, @@ -49,6 +51,8 @@ final class Fory { basicTypesRefIgnored: basicTypesRefIgnored, timeRefIgnored: timeRefIgnored, stringRefIgnored: stringRefIgnored, + maxBinarySize: maxBinarySize, + maxCollectionSize: maxCollectionSize, ), ); diff --git a/dart/packages/fory/lib/src/serializer/array_serializer.dart b/dart/packages/fory/lib/src/serializer/array_serializer.dart index e164afa158..6ad67ded58 100644 --- a/dart/packages/fory/lib/src/serializer/array_serializer.dart +++ b/dart/packages/fory/lib/src/serializer/array_serializer.dart @@ -18,7 +18,9 @@ */ import 'dart:typed_data'; +import 'package:fory/src/const/types.dart'; import 'package:fory/src/deserialization_context.dart'; +import 'package:fory/src/exception/fory_exception.dart'; import 'package:fory/src/memory/byte_reader.dart'; import 'package:fory/src/memory/byte_writer.dart'; import 'package:fory/src/serialization_context.dart'; @@ -60,6 +62,11 @@ abstract base class NumericArraySerializer @override TypedDataList read(ByteReader br, int refId, DeserializationContext pack) { int numBytes = br.readVarUint32Small7(); + if (objType == ObjType.BINARY && numBytes > pack.config.maxBinarySize) { + throw InvalidDataException( + 'Binary size $numBytes exceeds maxBinarySize ${pack.config.maxBinarySize}. ' + 'The input data may be malicious, or need to increase the maxBinarySize when creating Fory.'); + } int length = numBytes ~/ bytesPerNum; if (isLittleEndian || bytesPerNum == 1) { // Fast path: direct memory copy on little-endian or for single-byte types diff --git a/dart/packages/fory/lib/src/serializer/collection/list/list_serializer.dart b/dart/packages/fory/lib/src/serializer/collection/list/list_serializer.dart index 55e75c43da..67bb80f0d7 100644 --- a/dart/packages/fory/lib/src/serializer/collection/list/list_serializer.dart +++ b/dart/packages/fory/lib/src/serializer/collection/list/list_serializer.dart @@ -20,6 +20,7 @@ import 'package:fory/src/const/ref_flag.dart'; import 'package:fory/src/const/types.dart'; import 'package:fory/src/deserialization_context.dart'; +import 'package:fory/src/exception/fory_exception.dart'; import 'package:fory/src/memory/byte_reader.dart'; import 'package:fory/src/meta/spec_wraps/type_spec_wrap.dart'; import 'package:fory/src/serializer/collection/iterable_serializer.dart'; @@ -33,6 +34,11 @@ abstract base class ListSerializer extends IterableSerializer { @override List read(ByteReader br, int refId, DeserializationContext pack) { int num = br.readVarUint32Small7(); + if (num > pack.config.maxCollectionSize) { + throw InvalidDataException( + 'List size $num exceeds maxCollectionSize ${pack.config.maxCollectionSize}. ' + 'The input data may be malicious, or need to increase the maxCollectionSize when creating Fory.'); + } TypeSpecWrap? elemWrap = pack.typeWrapStack.peek?.param0; List list = newList( num, diff --git a/dart/packages/fory/lib/src/serializer/collection/map/map_serializer.dart b/dart/packages/fory/lib/src/serializer/collection/map/map_serializer.dart index 27e698a167..8176b87163 100644 --- a/dart/packages/fory/lib/src/serializer/collection/map/map_serializer.dart +++ b/dart/packages/fory/lib/src/serializer/collection/map/map_serializer.dart @@ -18,6 +18,7 @@ */ import 'package:fory/src/deserialization_context.dart'; +import 'package:fory/src/exception/fory_exception.dart'; import 'package:fory/src/meta/spec_wraps/type_spec_wrap.dart'; import 'package:fory/src/const/types.dart'; import 'package:fory/src/memory/byte_reader.dart'; @@ -51,6 +52,11 @@ abstract base class MapSerializer> @override T read(ByteReader br, int refId, DeserializationContext pack) { int remaining = br.readVarUint32Small7(); + if (remaining > pack.config.maxCollectionSize) { + throw InvalidDataException( + 'Map size $remaining exceeds maxCollectionSize ${pack.config.maxCollectionSize}. ' + 'The input data may be malicious, or need to increase the maxCollectionSize when creating Fory.'); + } T map = newMap(remaining); if (writeRef) { pack.refResolver.setRefTheLatestId(map); diff --git a/dart/packages/fory/lib/src/serializer/collection/set/set_serializer.dart b/dart/packages/fory/lib/src/serializer/collection/set/set_serializer.dart index a0622c1a53..b2612f29d7 100644 --- a/dart/packages/fory/lib/src/serializer/collection/set/set_serializer.dart +++ b/dart/packages/fory/lib/src/serializer/collection/set/set_serializer.dart @@ -28,6 +28,7 @@ library; import 'package:fory/src/const/ref_flag.dart'; import 'package:fory/src/const/types.dart'; import 'package:fory/src/deserialization_context.dart'; +import 'package:fory/src/exception/fory_exception.dart'; import 'package:fory/src/memory/byte_reader.dart'; import 'package:fory/src/meta/spec_wraps/type_spec_wrap.dart'; import 'package:fory/src/serializer/collection/iterable_serializer.dart'; @@ -41,6 +42,11 @@ abstract base class SetSerializer extends IterableSerializer { @override Set read(ByteReader br, int refId, DeserializationContext pack) { int num = br.readVarUint32Small7(); + if (num > pack.config.maxCollectionSize) { + throw InvalidDataException( + 'Set size $num exceeds maxCollectionSize ${pack.config.maxCollectionSize}. ' + 'The input data may be malicious, or need to increase the maxCollectionSize when creating Fory.'); + } TypeSpecWrap? elemWrap = pack.typeWrapStack.peek?.param0; Set set = newSet( elemWrap == null || elemWrap.nullable,