Loading core/modules/jsonapi/tests/modules/jsonapi_test_field_type/config/schema/jsonapi_test_field_type.schema.yml +12 −0 Changes for core/modules/jsonapi/tests/modules/jsonapi_test_field_type/config/schema/jsonapi_test_field_type.schema.yml: 12 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -9,3 +9,15 @@ field.field_settings.jsonapi_test_field_type_entity_reference_uuid: field.value.jsonapi_test_field_type_entity_reference_uuid: type: field.value.entity_reference label: 'Default value' field.storage_settings.jsonapi_test_field_type_entity_reference_with_meta: type: field.storage_settings.entity_reference label: 'Entity reference with meta field storage settings' field.field_settings.jsonapi_test_field_type_entity_reference_with_meta: type: field.field_settings.entity_reference label: 'Entity reference with meta field settings' field.value.jsonapi_test_field_type_entity_reference_with_meta: type: field.value.entity_reference label: 'Default value' core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/Plugin/Field/FieldType/EntityReferenceWithMetaItem.php 0 → 100644 +61 −0 Changes for core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/Plugin/Field/FieldType/EntityReferenceWithMetaItem.php: 61 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\jsonapi_test_field_type\Plugin\Field\FieldType; use Drupal\Core\Field\Attribute\FieldType; use Drupal\Core\Field\EntityReferenceFieldItemList; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\TypedData\DataDefinition; /** * Defines the 'entity_reference_with_meta' field type. * * An entity reference field type with an additional data property, used to * expose the conditions required to test JSON:API relationship PATCH handling. * * @see https://www.drupal.org/project/drupal/issues/XXXXXXX */ #[FieldType( id: 'jsonapi_test_field_type_entity_reference_with_meta', label: new TranslatableMarkup('Entity reference with meta'), description: new TranslatableMarkup('Test field type: entity reference with an additional data property.'), category: 'reference', default_widget: 'entity_reference_autocomplete', default_formatter: 'entity_reference_label', list_class: EntityReferenceFieldItemList::class, serialized_property_names: ['data'], )] class EntityReferenceWithMetaItem extends EntityReferenceItem { /** * {@inheritdoc} */ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition): array { $properties = parent::propertyDefinitions($field_definition); $properties['data'] = DataDefinition::create('string') ->setLabel(new TranslatableMarkup('Data')) ->setRequired(FALSE); return $properties; } /** * {@inheritdoc} */ public static function schema(FieldStorageDefinitionInterface $field_definition): array { $schema = parent::schema($field_definition); $schema['columns']['data'] = [ 'type' => 'blob', 'size' => 'big', ]; return $schema; } } core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/TestGadget.php 0 → 100644 +25 −0 Changes for core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/TestGadget.php: 25 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\jsonapi_test_field_type; /** * A test gadget class for verifying that object injection is blocked. * * If unserialize() is called without allowed_classes: false, this class will * be instantiated and __wakeup() will throw an Exception. The presence of the * Exception proves that Object Injection was successful. * * @internal */ class TestGadget { /** * {@inheritdoc} */ public function __wakeup(): void { throw new \RuntimeException('JSONAPI_TEST_GADGET_WAKEUP_CALLED'); } } core/modules/jsonapi/tests/src/Functional/JsonApiRelationshipTest.php +224 −0 Changes for core/modules/jsonapi/tests/src/Functional/JsonApiRelationshipTest.php: 224 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ use Drupal\entity_test\EntityTestHelper; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\jsonapi_test_field_type\TestGadget; use GuzzleHttp\RequestOptions; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; Loading Loading @@ -45,6 +46,16 @@ class JsonApiRelationshipTest extends JsonApiFunctionalTestBase { */ protected string $fieldName = 'field_child'; /** * The field name for the to-many entity reference with meta field. */ protected string $fieldNameWithMeta = 'field_ref_with_meta'; /** * The field name for the to-one entity reference with meta field. */ protected string $fieldNameWithMetaSingle = 'field_ref_with_meta_single'; /** * {@inheritdoc} */ Loading Loading @@ -73,6 +84,48 @@ protected function setUp(): void { ], ])->save(); // To-many field for PATCH and POST tests. FieldStorageConfig::create([ 'field_name' => $this->fieldNameWithMeta, 'type' => 'jsonapi_test_field_type_entity_reference_with_meta', 'entity_type' => $this->entityTypeId, 'cardinality' => -1, 'settings' => [ 'target_type' => $this->entityTypeId, ], ])->save(); FieldConfig::create([ 'field_name' => $this->fieldNameWithMeta, 'entity_type' => $this->entityTypeId, 'bundle' => $this->bundle, 'label' => $this->randomString(), 'settings' => [ 'handler' => 'default', 'handler_settings' => [], ], ])->save(); // To-one field for PATCH single-value test. FieldStorageConfig::create([ 'field_name' => $this->fieldNameWithMetaSingle, 'type' => 'jsonapi_test_field_type_entity_reference_with_meta', 'entity_type' => $this->entityTypeId, 'cardinality' => 1, 'settings' => [ 'target_type' => $this->entityTypeId, ], ])->save(); FieldConfig::create([ 'field_name' => $this->fieldNameWithMetaSingle, 'entity_type' => $this->entityTypeId, 'bundle' => $this->bundle, 'label' => $this->randomString(), 'settings' => [ 'handler' => 'default', 'handler_settings' => [], ], ])->save(); \Drupal::service('router.builder')->rebuild(); } Loading Loading @@ -156,4 +209,175 @@ public function testPatchHandleUUIDPropertyReferenceFieldIssue3127883(): void { $this->assertEquals($childEntity->uuid(), $parentEntity->get($this->fieldName)->target_uuid); } /** * PATCH to a to-many relationship does not allow object injection. * * A field type combining EntityReferenceItem lineage with * serialized_property_names exposes a code path where user-supplied data * in meta.data is passed to unserialize(). This test confirms that a gadget * chain payload does not result in object instantiation. */ public function testMultiValueRelationshipPatchDoesNotAllowObjectInjection(): void { $this->config('jsonapi.settings')->set('read_only', FALSE)->save(); $user = $this->drupalCreateUser([ 'view test entity', 'edit own entity_test content', ]); $storage = $this->container->get('entity_type.manager') ->getStorage($this->entityTypeId); $parentEntity = $storage->create([ 'type' => $this->bundle, 'user_id' => $user->id(), ]); $parentEntity->save(); $targetEntity = $storage->create(['type' => $this->bundle]); $targetEntity->save(); $url = Url::fromUri(sprintf( 'internal:/jsonapi/%s/%s/%s/relationships/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid(), $this->fieldNameWithMeta, )); $response = $this->request('PATCH', $url, [ RequestOptions::HEADERS => [ 'Content-Type' => 'application/vnd.api+json', 'Accept' => 'application/vnd.api+json', ], RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw], RequestOptions::JSON => [ 'data' => [ [ 'id' => $targetEntity->uuid(), 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle), 'meta' => [ 'data' => serialize(new TestGadget()), ], ], ], ], ]); $this->assertStringNotContainsString( 'JSONAPI_TEST_GADGET_WAKEUP_CALLED', (string) $response->getBody(), 'Object injection succeeded: TestGadget::__wakeup() was called.', ); } /** * PATCH to a to-one relationship does not allow object injection. */ public function testSingleValueRelationshipPatchDoesNotAllowObjectInjection(): void { $this->config('jsonapi.settings')->set('read_only', FALSE)->save(); $user = $this->drupalCreateUser([ 'view test entity', 'edit own entity_test content', ]); $storage = $this->container->get('entity_type.manager') ->getStorage($this->entityTypeId); $parentEntity = $storage->create([ 'type' => $this->bundle, 'user_id' => $user->id(), ]); $parentEntity->save(); $targetEntity = $storage->create(['type' => $this->bundle]); $targetEntity->save(); $url = Url::fromUri(sprintf( 'internal:/jsonapi/%s/%s/%s/relationships/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid(), $this->fieldNameWithMetaSingle, )); $response = $this->request('PATCH', $url, [ RequestOptions::HEADERS => [ 'Content-Type' => 'application/vnd.api+json', 'Accept' => 'application/vnd.api+json', ], RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw], RequestOptions::JSON => [ 'data' => [ 'id' => $targetEntity->uuid(), 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle), 'meta' => [ 'data' => serialize(new TestGadget()), ], ], ], ]); $this->assertStringNotContainsString( 'JSONAPI_TEST_GADGET_WAKEUP_CALLED', (string) $response->getBody(), 'Object injection succeeded: TestGadget::__wakeup() was called.', ); } /** * POST to a to-many relationship does not allow object injection. */ public function testMultiValueRelationshipPostDoesNotAllowObjectInjection(): void { $this->config('jsonapi.settings')->set('read_only', FALSE)->save(); $user = $this->drupalCreateUser([ 'view test entity', 'edit own entity_test content', ]); $storage = $this->container->get('entity_type.manager') ->getStorage($this->entityTypeId); $parentEntity = $storage->create([ 'type' => $this->bundle, 'user_id' => $user->id(), ]); $parentEntity->save(); $targetEntity = $storage->create(['type' => $this->bundle]); $targetEntity->save(); $url = Url::fromUri(sprintf( 'internal:/jsonapi/%s/%s/%s/relationships/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid(), $this->fieldNameWithMeta, )); $response = $this->request('POST', $url, [ RequestOptions::HEADERS => [ 'Content-Type' => 'application/vnd.api+json', 'Accept' => 'application/vnd.api+json', ], RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw], RequestOptions::JSON => [ 'data' => [ [ 'id' => $targetEntity->uuid(), 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle), 'meta' => [ 'data' => serialize(new TestGadget()), ], ], ], ], ]); $this->assertStringNotContainsString( 'JSONAPI_TEST_GADGET_WAKEUP_CALLED', (string) $response->getBody(), 'Object injection succeeded: TestGadget::__wakeup() was called.', ); } } Loading
core/modules/jsonapi/tests/modules/jsonapi_test_field_type/config/schema/jsonapi_test_field_type.schema.yml +12 −0 Changes for core/modules/jsonapi/tests/modules/jsonapi_test_field_type/config/schema/jsonapi_test_field_type.schema.yml: 12 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -9,3 +9,15 @@ field.field_settings.jsonapi_test_field_type_entity_reference_uuid: field.value.jsonapi_test_field_type_entity_reference_uuid: type: field.value.entity_reference label: 'Default value' field.storage_settings.jsonapi_test_field_type_entity_reference_with_meta: type: field.storage_settings.entity_reference label: 'Entity reference with meta field storage settings' field.field_settings.jsonapi_test_field_type_entity_reference_with_meta: type: field.field_settings.entity_reference label: 'Entity reference with meta field settings' field.value.jsonapi_test_field_type_entity_reference_with_meta: type: field.value.entity_reference label: 'Default value'
core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/Plugin/Field/FieldType/EntityReferenceWithMetaItem.php 0 → 100644 +61 −0 Changes for core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/Plugin/Field/FieldType/EntityReferenceWithMetaItem.php: 61 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\jsonapi_test_field_type\Plugin\Field\FieldType; use Drupal\Core\Field\Attribute\FieldType; use Drupal\Core\Field\EntityReferenceFieldItemList; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\TypedData\DataDefinition; /** * Defines the 'entity_reference_with_meta' field type. * * An entity reference field type with an additional data property, used to * expose the conditions required to test JSON:API relationship PATCH handling. * * @see https://www.drupal.org/project/drupal/issues/XXXXXXX */ #[FieldType( id: 'jsonapi_test_field_type_entity_reference_with_meta', label: new TranslatableMarkup('Entity reference with meta'), description: new TranslatableMarkup('Test field type: entity reference with an additional data property.'), category: 'reference', default_widget: 'entity_reference_autocomplete', default_formatter: 'entity_reference_label', list_class: EntityReferenceFieldItemList::class, serialized_property_names: ['data'], )] class EntityReferenceWithMetaItem extends EntityReferenceItem { /** * {@inheritdoc} */ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition): array { $properties = parent::propertyDefinitions($field_definition); $properties['data'] = DataDefinition::create('string') ->setLabel(new TranslatableMarkup('Data')) ->setRequired(FALSE); return $properties; } /** * {@inheritdoc} */ public static function schema(FieldStorageDefinitionInterface $field_definition): array { $schema = parent::schema($field_definition); $schema['columns']['data'] = [ 'type' => 'blob', 'size' => 'big', ]; return $schema; } }
core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/TestGadget.php 0 → 100644 +25 −0 Changes for core/modules/jsonapi/tests/modules/jsonapi_test_field_type/src/TestGadget.php: 25 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\jsonapi_test_field_type; /** * A test gadget class for verifying that object injection is blocked. * * If unserialize() is called without allowed_classes: false, this class will * be instantiated and __wakeup() will throw an Exception. The presence of the * Exception proves that Object Injection was successful. * * @internal */ class TestGadget { /** * {@inheritdoc} */ public function __wakeup(): void { throw new \RuntimeException('JSONAPI_TEST_GADGET_WAKEUP_CALLED'); } }
core/modules/jsonapi/tests/src/Functional/JsonApiRelationshipTest.php +224 −0 Changes for core/modules/jsonapi/tests/src/Functional/JsonApiRelationshipTest.php: 224 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ use Drupal\entity_test\EntityTestHelper; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\jsonapi_test_field_type\TestGadget; use GuzzleHttp\RequestOptions; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; Loading Loading @@ -45,6 +46,16 @@ class JsonApiRelationshipTest extends JsonApiFunctionalTestBase { */ protected string $fieldName = 'field_child'; /** * The field name for the to-many entity reference with meta field. */ protected string $fieldNameWithMeta = 'field_ref_with_meta'; /** * The field name for the to-one entity reference with meta field. */ protected string $fieldNameWithMetaSingle = 'field_ref_with_meta_single'; /** * {@inheritdoc} */ Loading Loading @@ -73,6 +84,48 @@ protected function setUp(): void { ], ])->save(); // To-many field for PATCH and POST tests. FieldStorageConfig::create([ 'field_name' => $this->fieldNameWithMeta, 'type' => 'jsonapi_test_field_type_entity_reference_with_meta', 'entity_type' => $this->entityTypeId, 'cardinality' => -1, 'settings' => [ 'target_type' => $this->entityTypeId, ], ])->save(); FieldConfig::create([ 'field_name' => $this->fieldNameWithMeta, 'entity_type' => $this->entityTypeId, 'bundle' => $this->bundle, 'label' => $this->randomString(), 'settings' => [ 'handler' => 'default', 'handler_settings' => [], ], ])->save(); // To-one field for PATCH single-value test. FieldStorageConfig::create([ 'field_name' => $this->fieldNameWithMetaSingle, 'type' => 'jsonapi_test_field_type_entity_reference_with_meta', 'entity_type' => $this->entityTypeId, 'cardinality' => 1, 'settings' => [ 'target_type' => $this->entityTypeId, ], ])->save(); FieldConfig::create([ 'field_name' => $this->fieldNameWithMetaSingle, 'entity_type' => $this->entityTypeId, 'bundle' => $this->bundle, 'label' => $this->randomString(), 'settings' => [ 'handler' => 'default', 'handler_settings' => [], ], ])->save(); \Drupal::service('router.builder')->rebuild(); } Loading Loading @@ -156,4 +209,175 @@ public function testPatchHandleUUIDPropertyReferenceFieldIssue3127883(): void { $this->assertEquals($childEntity->uuid(), $parentEntity->get($this->fieldName)->target_uuid); } /** * PATCH to a to-many relationship does not allow object injection. * * A field type combining EntityReferenceItem lineage with * serialized_property_names exposes a code path where user-supplied data * in meta.data is passed to unserialize(). This test confirms that a gadget * chain payload does not result in object instantiation. */ public function testMultiValueRelationshipPatchDoesNotAllowObjectInjection(): void { $this->config('jsonapi.settings')->set('read_only', FALSE)->save(); $user = $this->drupalCreateUser([ 'view test entity', 'edit own entity_test content', ]); $storage = $this->container->get('entity_type.manager') ->getStorage($this->entityTypeId); $parentEntity = $storage->create([ 'type' => $this->bundle, 'user_id' => $user->id(), ]); $parentEntity->save(); $targetEntity = $storage->create(['type' => $this->bundle]); $targetEntity->save(); $url = Url::fromUri(sprintf( 'internal:/jsonapi/%s/%s/%s/relationships/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid(), $this->fieldNameWithMeta, )); $response = $this->request('PATCH', $url, [ RequestOptions::HEADERS => [ 'Content-Type' => 'application/vnd.api+json', 'Accept' => 'application/vnd.api+json', ], RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw], RequestOptions::JSON => [ 'data' => [ [ 'id' => $targetEntity->uuid(), 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle), 'meta' => [ 'data' => serialize(new TestGadget()), ], ], ], ], ]); $this->assertStringNotContainsString( 'JSONAPI_TEST_GADGET_WAKEUP_CALLED', (string) $response->getBody(), 'Object injection succeeded: TestGadget::__wakeup() was called.', ); } /** * PATCH to a to-one relationship does not allow object injection. */ public function testSingleValueRelationshipPatchDoesNotAllowObjectInjection(): void { $this->config('jsonapi.settings')->set('read_only', FALSE)->save(); $user = $this->drupalCreateUser([ 'view test entity', 'edit own entity_test content', ]); $storage = $this->container->get('entity_type.manager') ->getStorage($this->entityTypeId); $parentEntity = $storage->create([ 'type' => $this->bundle, 'user_id' => $user->id(), ]); $parentEntity->save(); $targetEntity = $storage->create(['type' => $this->bundle]); $targetEntity->save(); $url = Url::fromUri(sprintf( 'internal:/jsonapi/%s/%s/%s/relationships/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid(), $this->fieldNameWithMetaSingle, )); $response = $this->request('PATCH', $url, [ RequestOptions::HEADERS => [ 'Content-Type' => 'application/vnd.api+json', 'Accept' => 'application/vnd.api+json', ], RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw], RequestOptions::JSON => [ 'data' => [ 'id' => $targetEntity->uuid(), 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle), 'meta' => [ 'data' => serialize(new TestGadget()), ], ], ], ]); $this->assertStringNotContainsString( 'JSONAPI_TEST_GADGET_WAKEUP_CALLED', (string) $response->getBody(), 'Object injection succeeded: TestGadget::__wakeup() was called.', ); } /** * POST to a to-many relationship does not allow object injection. */ public function testMultiValueRelationshipPostDoesNotAllowObjectInjection(): void { $this->config('jsonapi.settings')->set('read_only', FALSE)->save(); $user = $this->drupalCreateUser([ 'view test entity', 'edit own entity_test content', ]); $storage = $this->container->get('entity_type.manager') ->getStorage($this->entityTypeId); $parentEntity = $storage->create([ 'type' => $this->bundle, 'user_id' => $user->id(), ]); $parentEntity->save(); $targetEntity = $storage->create(['type' => $this->bundle]); $targetEntity->save(); $url = Url::fromUri(sprintf( 'internal:/jsonapi/%s/%s/%s/relationships/%s', $this->entityTypeId, $this->bundle, $parentEntity->uuid(), $this->fieldNameWithMeta, )); $response = $this->request('POST', $url, [ RequestOptions::HEADERS => [ 'Content-Type' => 'application/vnd.api+json', 'Accept' => 'application/vnd.api+json', ], RequestOptions::AUTH => [$user->getAccountName(), $user->pass_raw], RequestOptions::JSON => [ 'data' => [ [ 'id' => $targetEntity->uuid(), 'type' => sprintf('%s--%s', $this->entityTypeId, $this->bundle), 'meta' => [ 'data' => serialize(new TestGadget()), ], ], ], ], ]); $this->assertStringNotContainsString( 'JSONAPI_TEST_GADGET_WAKEUP_CALLED', (string) $response->getBody(), 'Object injection succeeded: TestGadget::__wakeup() was called.', ); } }