Verified Commit ec88439a authored by catch's avatar catch Committed by Jess
Browse files

task: #3633173 Add test coverage for SA-CORE-2026-005

By: xjm
By: bbrala
By: longwave
By: larowlan
By: berdir
By: mcdruid
(cherry picked from commit 12547c8e)
parent dfcdf21f
Loading
Loading
Loading
Loading
+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
@@ -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'
+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;
  }

}
+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');
  }

}
+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
@@ -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;
@@ -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}
   */
@@ -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();
  }

@@ -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.',
    );
  }

}