diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 5cb123b..b9aa64f 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -12,24 +12,25 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-version: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + php-version: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] name: PHP ${{ matrix.php-version }} steps: - uses: actions/checkout@v6 + + - name: Set up PHP ${{ matrix.php-version }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring + - name: Validate composer.json and composer.lock run: composer validate - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v5 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- + - name: Install suggested dependencies - run: composer require jeremeamia/superclosure opis/closure - - name: Install dependencies - if: steps.composer-cache.outputs.cache-hit != 'true' - run: composer install --prefer-dist --no-progress --no-suggest + run: composer require jeremeamia/superclosure opis/closure --no-update --no-interaction --ansi + + - name: Install Composer dependencies + uses: ramsey/composer-install@v4 + - name: Run test suite run: vendor/bin/phpunit diff --git a/phpunit.xml b/phpunit.xml index 8e5ecc0..3c6fc88 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -9,7 +9,9 @@ colors="true"> - ./tests + ./tests/EnumSerializationTest.php + ./tests/JsonSerializerTest.php + ./tests/ClosureSerializer diff --git a/src/JsonSerializer/JsonSerializer.php b/src/JsonSerializer/JsonSerializer.php index 4b16ea2..231d268 100644 --- a/src/JsonSerializer/JsonSerializer.php +++ b/src/JsonSerializer/JsonSerializer.php @@ -308,10 +308,10 @@ protected function serializeData($value) */ protected function serializeObject($value) { - if ($this->objectStorage->contains($value)) { + if ($this->objectStorage->offsetExists($value)) { return [static::CLASS_IDENTIFIER_KEY => '@' . $this->objectStorage[$value]]; } - $this->objectStorage->attach($value, $this->objectMappingIndex++); + $this->objectStorage->offsetSet($value, $this->objectMappingIndex++); $ref = new ReflectionClass($value); $className = $ref->getName(); @@ -402,8 +402,10 @@ protected function extractObjectData($value, $ref, $properties) foreach ($properties as $property) { try { $propRef = $this->getReflectionProperty($ref, $property); - $propRef->setAccessible(true); - if (!$propRef->isInitialized($value)) { + if (PHP_VERSION_ID < 80100) { + $propRef->setAccessible(true); + } + if (PHP_VERSION_ID >= 70400 && !$propRef->isInitialized($value)) { continue; } $data[$property] = $propRef->getValue($value); @@ -548,7 +550,9 @@ protected function unserializeObject($value) foreach ($value as $property => $propertyValue) { try { $propRef = $this->getReflectionProperty($ref, $property); - $propRef->setAccessible(true); + if (PHP_VERSION_ID < 80100) { + $propRef->setAccessible(true); + } $propRef->setValue($obj, $this->unserializeData($propertyValue)); } catch (ReflectionException $e) { switch ($this->undefinedAttributeMode) { diff --git a/tests/ClosureSerializer/ClosureSerializerManagerTest.php b/tests/ClosureSerializer/ClosureSerializerManagerTest.php index b52abf4..f31fc2d 100644 --- a/tests/ClosureSerializer/ClosureSerializerManagerTest.php +++ b/tests/ClosureSerializer/ClosureSerializerManagerTest.php @@ -10,7 +10,7 @@ class ClosureSerializerManagerTest extends TestCase { public function setUp(): void { - if (! class_exists(\SuperClosure\SerializerInterface::class)) { + if (! interface_exists(\SuperClosure\SerializerInterface::class)) { $this->markTestSkipped('Missing jeremeamia/superclosure to run this test'); } } diff --git a/tests/ClosureSerializer/SuperClosureSerializerTest.php b/tests/ClosureSerializer/SuperClosureSerializerTest.php index 092220e..0684738 100644 --- a/tests/ClosureSerializer/SuperClosureSerializerTest.php +++ b/tests/ClosureSerializer/SuperClosureSerializerTest.php @@ -9,7 +9,7 @@ class SuperClosureSerializerTest extends TestCase { public function setUp(): void { - if (! class_exists(\SuperClosure\SerializerInterface::class)) { + if (! interface_exists(\SuperClosure\SerializerInterface::class)) { $this->markTestSkipped('Missing jeremeamia/superclosure to run this test'); } } diff --git a/tests/EnumSerializationTest.php b/tests/EnumSerializationTest.php new file mode 100644 index 0000000..f456c82 --- /dev/null +++ b/tests/EnumSerializationTest.php @@ -0,0 +1,145 @@ +serializer = new JsonSerializer(null, $customObjectSerializerMap); + } + + /** + * Test serialization of Enums + * + * @return void + */ + public function testSerializeEnums() + { + $unitEnum = SupportEnums\MyUnitEnum::Hearts; + $expected = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyUnitEnum","name":"Hearts"}'; + $this->assertSame($expected, $this->serializer->serialize($unitEnum)); + + $backedEnum = SupportEnums\MyBackedEnum::Hearts; + $expected = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyBackedEnum","name":"Hearts","value":"H"}'; + $this->assertSame($expected, $this->serializer->serialize($backedEnum)); + + $intBackedEnum = SupportEnums\MyIntBackedEnum::One; + $expected = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyIntBackedEnum","name":"One","value":1}'; + $this->assertSame($expected, $this->serializer->serialize($intBackedEnum)); + } + + /** + * Test serialization of multiple Enums + * + * @return void + */ + public function testSerializeMultipleEnums() + { + $obj = new stdClass(); + $obj->enum1 = SupportEnums\MyUnitEnum::Hearts; + $obj->enum2 = SupportEnums\MyBackedEnum::Hearts; + $obj->enum3 = SupportEnums\MyIntBackedEnum::One; + $obj->enum4 = SupportEnums\MyUnitEnum::Hearts; + $obj->enum5 = SupportEnums\MyBackedEnum::Hearts; + $obj->enum6 = SupportEnums\MyIntBackedEnum::One; + + $expected = '{"@type":"stdClass","enum1":{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyUnitEnum","name":"Hearts"},"enum2":{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyBackedEnum","name":"Hearts","value":"H"},"enum3":{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyIntBackedEnum","name":"One","value":1},"enum4":{"@type":"@1"},"enum5":{"@type":"@2"},"enum6":{"@type":"@3"}}'; + $this->assertSame($expected, $this->serializer->serialize($obj)); + } + + /** + * Test unserialization of Enums + * + * @return void + */ + public function testUnserializeEnums() + { + $serialized = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyUnitEnum","name":"Hearts"}'; + $obj = $this->serializer->unserialize($serialized); + $this->assertInstanceOf('Zumba\JsonSerializer\Test\SupportEnums\MyUnitEnum', $obj); + $this->assertSame(SupportEnums\MyUnitEnum::Hearts, $obj); + + $serialized = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyBackedEnum","name":"Hearts","value":"H"}'; + $obj = $this->serializer->unserialize($serialized); + $this->assertInstanceOf('Zumba\JsonSerializer\Test\SupportEnums\MyBackedEnum', $obj); + $this->assertSame(SupportEnums\MyBackedEnum::Hearts, $obj); + + $serialized = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyIntBackedEnum","name":"Two","value":2}'; + $obj = $this->serializer->unserialize($serialized); + $this->assertInstanceOf('Zumba\JsonSerializer\Test\SupportEnums\MyIntBackedEnum', $obj); + $this->assertSame(SupportEnums\MyIntBackedEnum::Two, $obj); + $this->assertSame(SupportEnums\MyIntBackedEnum::Two->value, $obj->value); + + // wrong value of BackedEnum is ignored + $serialized = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyBackedEnum","name":"Hearts","value":"S"}'; + $obj = $this->serializer->unserialize($serialized); + $this->assertInstanceOf('Zumba\JsonSerializer\Test\SupportEnums\MyBackedEnum', $obj); + $this->assertSame(SupportEnums\MyBackedEnum::Hearts, $obj); + $this->assertSame(SupportEnums\MyBackedEnum::Hearts->value, $obj->value); + } + + /** + * Test unserialization of multiple Enums + * + * @return void + */ + public function testUnserializeMultipleEnums() + { + $obj = new stdClass(); + $obj->enum1 = SupportEnums\MyUnitEnum::Hearts; + $obj->enum2 = SupportEnums\MyBackedEnum::Hearts; + $obj->enum3 = SupportEnums\MyIntBackedEnum::One; + $obj->enum4 = SupportEnums\MyUnitEnum::Hearts; + $obj->enum5 = SupportEnums\MyBackedEnum::Hearts; + $obj->enum6 = SupportEnums\MyIntBackedEnum::One; + + $serialized = '{"@type":"stdClass","enum1":{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyUnitEnum","name":"Hearts"},"enum2":{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyBackedEnum","name":"Hearts","value":"H"},"enum3":{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyIntBackedEnum","name":"One","value":1},"enum4":{"@type":"@1"},"enum5":{"@type":"@2"},"enum6":{"@type":"@3"}}'; + $actualObj = $this->serializer->unserialize($serialized); + $this->assertInstanceOf('stdClass', $actualObj); + $this->assertEquals($obj, $actualObj); + } + + /** + * Test unserialization of wrong UnitEnum + * + * @return void + */ + public function testUnserializeWrongUnitEnum() { + // bad case generate Error + $serialized = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyUnitEnum","name":"Circles"}'; + $this->expectException(\Error::class); + $this->serializer->unserialize($serialized); + } + + /** + * Test unserialization of wrong BackedEnum + * + * @return void + */ + public function testUnserializeWrongBackedEnum() { + // bad case generate Error + $serialized = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyBackedEnum","name":"Circles","value":"C"}'; + $this->expectException(\Error::class); + $this->serializer->unserialize($serialized); + } +} diff --git a/tests/JsonSerializerTest.php b/tests/JsonSerializerTest.php index f6bdb55..ef23482 100644 --- a/tests/JsonSerializerTest.php +++ b/tests/JsonSerializerTest.php @@ -230,10 +230,14 @@ public function testUnserializeObjects() $this->assertInstanceOf('Zumba\JsonSerializer\Test\SupportClasses\AllVisibilities', $obj); $this->assertInstanceOf('Zumba\JsonSerializer\Test\SupportClasses\EmptyClass', $obj->pub); $prop = new ReflectionProperty($obj, 'prot'); - $prop->setAccessible(true); + if (PHP_VERSION_ID < 80100) { + $prop->setAccessible(true); + } $this->assertSame('protected', $prop->getValue($obj)); $prop = new ReflectionProperty($obj, 'priv'); - $prop->setAccessible(true); + if (PHP_VERSION_ID < 80100) { + $prop->setAccessible(true); + } $this->assertSame('dont tell anyone', $prop->getValue($obj)); $serialized = '{"instance":{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportClasses\\\\EmptyClass"}}'; @@ -265,146 +269,6 @@ public function testSerializeObjectWithParentPrivateProperty() $this->assertSame('parentPublicValue', $restored->parentPublic); } - - /** - * Test serialization of Enums - * - * @return void - */ - public function testSerializeEnums() - { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped("Enums are only available since PHP 8.1"); - } - - $unitEnum = SupportEnums\MyUnitEnum::Hearts; - $expected = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyUnitEnum","name":"Hearts"}'; - $this->assertSame($expected, $this->serializer->serialize($unitEnum)); - - $backedEnum = SupportEnums\MyBackedEnum::Hearts; - $expected = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyBackedEnum","name":"Hearts","value":"H"}'; - $this->assertSame($expected, $this->serializer->serialize($backedEnum)); - - $intBackedEnum = SupportEnums\MyIntBackedEnum::One; - $expected = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyIntBackedEnum","name":"One","value":1}'; - $this->assertSame($expected, $this->serializer->serialize($intBackedEnum)); - } - - /** - * Test serialization of multiple Enums - * - * @return void - */ - public function testSerializeMultipleEnums() - { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped("Enums are only available since PHP 8.1"); - } - - $obj = new stdClass(); - $obj->enum1 = SupportEnums\MyUnitEnum::Hearts; - $obj->enum2 = SupportEnums\MyBackedEnum::Hearts; - $obj->enum3 = SupportEnums\MyIntBackedEnum::One; - $obj->enum4 = SupportEnums\MyUnitEnum::Hearts; - $obj->enum5 = SupportEnums\MyBackedEnum::Hearts; - $obj->enum6 = SupportEnums\MyIntBackedEnum::One; - - $expected = '{"@type":"stdClass","enum1":{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyUnitEnum","name":"Hearts"},"enum2":{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyBackedEnum","name":"Hearts","value":"H"},"enum3":{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyIntBackedEnum","name":"One","value":1},"enum4":{"@type":"@1"},"enum5":{"@type":"@2"},"enum6":{"@type":"@3"}}'; - $this->assertSame($expected, $this->serializer->serialize($obj)); - } - - /** - * Test unserialization of Enums - * - * @return void - */ - public function testUnserializeEnums() - { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped("Enums are only available since PHP 8.1"); - } - - $serialized = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyUnitEnum","name":"Hearts"}'; - $obj = $this->serializer->unserialize($serialized); - $this->assertInstanceOf('Zumba\JsonSerializer\Test\SupportEnums\MyUnitEnum', $obj); - $this->assertSame(SupportEnums\MyUnitEnum::Hearts, $obj); - - $serialized = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyBackedEnum","name":"Hearts","value":"H"}'; - $obj = $this->serializer->unserialize($serialized); - $this->assertInstanceOf('Zumba\JsonSerializer\Test\SupportEnums\MyBackedEnum', $obj); - $this->assertSame(SupportEnums\MyBackedEnum::Hearts, $obj); - - $serialized = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyIntBackedEnum","name":"Two","value":2}'; - $obj = $this->serializer->unserialize($serialized); - $this->assertInstanceOf('Zumba\JsonSerializer\Test\SupportEnums\MyIntBackedEnum', $obj); - $this->assertSame(SupportEnums\MyIntBackedEnum::Two, $obj); - $this->assertSame(SupportEnums\MyIntBackedEnum::Two->value, $obj->value); - - // wrong value of BackedEnum is ignored - $serialized = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyBackedEnum","name":"Hearts","value":"S"}'; - $obj = $this->serializer->unserialize($serialized); - $this->assertInstanceOf('Zumba\JsonSerializer\Test\SupportEnums\MyBackedEnum', $obj); - $this->assertSame(SupportEnums\MyBackedEnum::Hearts, $obj); - $this->assertSame(SupportEnums\MyBackedEnum::Hearts->value, $obj->value); - } - - /** - * Test unserialization of multiple Enums - * - * @return void - */ - public function testUnserializeMultipleEnums() - { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped("Enums are only available since PHP 8.1"); - } - - $obj = new stdClass(); - $obj->enum1 = SupportEnums\MyUnitEnum::Hearts; - $obj->enum2 = SupportEnums\MyBackedEnum::Hearts; - $obj->enum3 = SupportEnums\MyIntBackedEnum::One; - $obj->enum4 = SupportEnums\MyUnitEnum::Hearts; - $obj->enum5 = SupportEnums\MyBackedEnum::Hearts; - $obj->enum6 = SupportEnums\MyIntBackedEnum::One; - - $serialized = '{"@type":"stdClass","enum1":{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyUnitEnum","name":"Hearts"},"enum2":{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyBackedEnum","name":"Hearts","value":"H"},"enum3":{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyIntBackedEnum","name":"One","value":1},"enum4":{"@type":"@1"},"enum5":{"@type":"@2"},"enum6":{"@type":"@3"}}'; - $actualObj = $this->serializer->unserialize($serialized); - $this->assertInstanceOf('stdClass', $actualObj); - $this->assertEquals($obj, $actualObj); - } - - /** - * Test unserialization of wrong UnitEnum - * - * @return void - */ - public function testUnserializeWrongUnitEnum() { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped("Enums are only available since PHP 8.1"); - } - - // bad case generate Error - $serialized = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyUnitEnum","name":"Circles"}'; - $this->expectException(\Error::class); - $this->serializer->unserialize($serialized); - } - - /** - * Test unserialization of wrong BackedEnum - * - * @return void - */ - public function testUnserializeWrongBackedEnum() { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped("Enums are only available since PHP 8.1"); - } - - // bad case generate Error - $serialized = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportEnums\\\\MyBackedEnum","name":"Circles","value":"C"}'; - $this->expectException(\Error::class); - $this->serializer->unserialize($serialized); - } - /** * Test serialization of objects using the custom serializers * @@ -479,7 +343,20 @@ public function testSerializationOfClosureWithSuperClosureOnConstructor() } $closureSerializer = new SuperClosureSerializer(); + + $message = ''; + set_error_handler(static function (int $errno, string $errstr) use (&$message) { + $message = $errstr; + }, E_USER_DEPRECATED); + $serializer = new JsonSerializer($closureSerializer); + + restore_error_handler(); + $this->assertSame( + 'Passing a ClosureSerializerInterface to the constructor is deprecated and will be removed in 4.0.0. Use addClosureSerializer() instead.', + $message + ); + $serialized = $serializer->serialize( array( 'func' => function () { @@ -582,7 +459,6 @@ public function testSerializationOfClosureWitMultipleClosures() // Make sure it was serialized with SuperClosure $serialized = $serializer->serialize($serializeData); - echo $serialized; $this->assertGreaterThanOrEqual(0, strpos($serialized, 'SuperClosure')); $this->assertFalse(strpos($serialized, 'OpisClosure')); @@ -613,7 +489,8 @@ public function testUnserializeOfClosureWithoutSerializer() } $closureSerializer = new SuperClosureSerializer(); - $serializer = new JsonSerializer($closureSerializer); + $serializer = new JsonSerializer(); + $serializer->addClosureSerializer(new ClosureSerializer\SuperClosureSerializer($closureSerializer)); $serialized = $serializer->serialize( array( 'func' => function () { @@ -878,7 +755,10 @@ public function testSerializationOfSplDoublyLinkedList() * Test serialization of an object with an uninitialized typed property * * @return void + * + * @requires PHP >= 7.4 */ + #[\PHPUnit\Framework\Attributes\RequiresPhp('>= 7.4')] public function testSerializeObjectWithUninitializedTypedProperty() { $obj = new \Zumba\JsonSerializer\Test\SupportClasses\UninitializedTypedProperty(); @@ -932,7 +812,10 @@ public function testAllowedClassIsDeserialized(): void /** * A class NOT in the allowlist must throw JsonSerializerException and must * never have its __wakeup() or __destruct() triggered (gadget chain blocked). + * + * @requires PHP >= 7.4 */ + #[\PHPUnit\Framework\Attributes\RequiresPhp('>= 7.4')] public function testUnlistedClassIsRejectedAndMagicMethodsNotCalled(): void { SupportClasses\GadgetClass::$wakeupCalled = false; @@ -956,6 +839,36 @@ public function testUnlistedClassIsRejectedAndMagicMethodsNotCalled(): void ); } + /** + * A class NOT in the allowlist must throw JsonSerializerException and must + * never have its __wakeup() or __destruct() triggered (gadget chain blocked). + * + * @requires PHP < 7.4 + */ + #[\PHPUnit\Framework\Attributes\RequiresPhp('< 7.4')] + public function testUnlistedClassIsRejectedAndMagicMethodsNotCalled2(): void + { + SupportClasses\GadgetClassWithoutTypes::$wakeupCalled = false; + SupportClasses\GadgetClassWithoutTypes::$destructCalled = false; + + $this->serializer->setAllowedClasses(['stdClass']); // GadgetClass is NOT listed + + $payload = '{"@type":"Zumba\\\\JsonSerializer\\\\Test\\\\SupportClasses\\\\GadgetClassWithoutTypes","command":"id"}'; + + try { + $this->serializer->unserialize($payload); + $this->fail('Expected JsonSerializerException was not thrown.'); + } catch (JsonSerializerException $e) { + $this->assertStringContainsString('not allowed', $e->getMessage()); + } + + // Ensure neither magic method was executed. + $this->assertFalse( + SupportClasses\GadgetClassWithoutTypes::$wakeupCalled, + '__wakeup() must not be called on a blocked class.' + ); + } + /** * An empty allowlist must block every class, including stdClass. */ @@ -997,7 +910,10 @@ public function testSettingAllowedClassesToNullRestoresDefaultBehaviour(): void /** * Simulates the PoC from the security report: an attacker-supplied @type * pointing to a gadget class must be rejected when an allowlist is active. + * + * @requires PHP >= 7.4 */ + #[\PHPUnit\Framework\Attributes\RequiresPhp('>= 7.4')] public function testSecurityReportPoCIsBlockedByAllowlist(): void { $this->serializer->setAllowedClasses(['stdClass']); @@ -1012,4 +928,26 @@ public function testSecurityReportPoCIsBlockedByAllowlist(): void $this->expectException(JsonSerializerException::class); $this->serializer->unserialize($payload); } + + /** + * Simulates the PoC from the security report: an attacker-supplied @type + * pointing to a gadget class must be rejected when an allowlist is active. + * + * @requires PHP < 7.4 + */ + #[\PHPUnit\Framework\Attributes\RequiresPhp('< 7.4')] + public function testSecurityReportPoCIsBlockedByAllowlist2(): void + { + $this->serializer->setAllowedClasses(['stdClass']); + + // Payload from the security report (adapted to a class defined in this + // test suite so that class_exists() returns true). + $payload = json_encode([ + '@type' => 'Zumba\\JsonSerializer\\Test\\SupportClasses\\GadgetClassWithoutTypes', + 'command' => 'id', + ]); + + $this->expectException(JsonSerializerException::class); + $this->serializer->unserialize($payload); + } } diff --git a/tests/SupportClasses/GadgetClassWithoutTypes.php b/tests/SupportClasses/GadgetClassWithoutTypes.php new file mode 100644 index 0000000..6065b42 --- /dev/null +++ b/tests/SupportClasses/GadgetClassWithoutTypes.php @@ -0,0 +1,26 @@ +