Verified Commit 677b3b1b authored by godotislate's avatar godotislate
Browse files

fix: #3475540 Use a route requirement to prevent non-ASCII characters causing...

fix: #3475540 Use a route requirement to prevent non-ASCII characters causing an exception when looking up a config entity

By: jannakha
By: cilefen
By: vladimiraus
By: smustgrave
By: arunkumark
By: tobiasb
By: oily
By: dries
By: alexpott
By: catch
parent 39e95282
Loading
Loading
Loading
Loading
Loading
+5 −0
Changes for core/lib/Drupal/Core/ParamConverter/AdminPathConfigEntityConverter.php: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -73,6 +73,11 @@ public function convert($value, $definition, $name, array $defaults) {
      if (!$entity_type->entityClassImplements(ConfigEntityInterface::class)) {
        return parent::convert($value, $definition, $name, $defaults);
      }

      // Ensure the value is printable ASCII.
      if (!preg_match('/^' . static::PRINTABLE_ASCII . '$/', $value)) {
        return NULL;
      }
    }

    if ($storage = $this->entityTypeManager->getStorage($entity_type_id)) {
+42 −1
Changes for core/lib/Drupal/Core/ParamConverter/EntityConverter.php: 42 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -2,6 +2,8 @@

namespace Drupal\Core\ParamConverter;

use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
@@ -76,7 +78,16 @@
 *
 * @see entities_revisions_translations
 */
class EntityConverter implements ParamConverterInterface {
class EntityConverter implements ParamConverterInterface, ParamConverterRouteRequirementInterface {

  /**
   * Regex to match printable ASCII characters.
   *
   * Used to prevent database errors when loading configuration entities.
   * Configuration entities IDs have an even more limited character set
   * depending on their schema.
   */
  protected const string PRINTABLE_ASCII = '[\x20-\x7E]+';

  use DynamicEntityTypeParamConverterTrait;

@@ -115,6 +126,17 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Ent
  public function convert($value, $definition, $name, array $defaults) {
    $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults);

    // If the entity type is dynamic, confirm it to be a config entity. Static
    // entity types will have performed this check in self::applies().
    if (str_starts_with($definition['type'], 'entity:{')) {
      $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
      if ($entity_type->entityClassImplements(ConfigEntityInterface::class)) {
        if (!preg_match('/^' . static::PRINTABLE_ASCII . '$/', $value)) {
          return NULL;
        }
      }
    }

    // If the entity type is revisionable and the parameter has the
    // "load_latest_revision" flag, load the active variant.
    if (!empty($definition['load_latest_revision'])) {
@@ -159,4 +181,23 @@ public function applies($definition, $name, Route $route) {
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function getRouteRequirement($definition, $name): ?string {
    $entity_type_id = substr($definition['type'], strlen('entity:'));
    // Cannot determine requirements for dynamic entity types at build time.
    if (str_contains($entity_type_id, '{')) {
      return NULL;
    }

    $entity_type = $this->entityTypeManager->getDefinition($entity_type_id, FALSE);
    // Config entities: derive regex from config schema constraints.
    if ($entity_type instanceof ConfigEntityTypeInterface) {
      return static::PRINTABLE_ASCII;
    }

    return NULL;
  }

}
+12 −4
Changes for core/lib/Drupal/Core/ParamConverter/ParamConverterManager.php: 12 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -49,11 +49,8 @@ public function setRouteParameterConverters(RouteCollection $routes) {

      // Loop over all defined parameters and look up the right converter.
      foreach ($parameters as $name => &$definition) {
        if (isset($definition['converter'])) {
        // Skip parameters that already have a manually set converter.
          continue;
        }

        if (!isset($definition['converter'])) {
          foreach ($this->converters->getIterator() as $converter => $service) {
            if ($service->applies($definition, $name, $route)) {
              $definition['converter'] = $converter;
@@ -62,6 +59,17 @@ public function setRouteParameterConverters(RouteCollection $routes) {
          }
        }

        if (isset($definition['converter']) && !$route->hasRequirement($name)) {
          $service = $this->getConverter($definition['converter']);
          // If the converter can provide a regex requirement and the route
          // doesn't already have one for this parameter, set it.
          if ($service instanceof ParamConverterRouteRequirementInterface
            && $requirement = $service->getRouteRequirement($definition, $name)) {
            $route->setRequirement($name, $requirement);
          }
        }
      }

      // Override the parameters array.
      $route->setOption('parameters', $parameters);
    }
+24 −0
Changes for core/lib/Drupal/Core/ParamConverter/ParamConverterRouteRequirementInterface.php: 24 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Drupal\Core\ParamConverter;

/**
 * Interface for param converters providing a route requirement for parameters.
 */
interface ParamConverterRouteRequirementInterface {

  /**
   * Returns a regex requirement for a route parameter.
   *
   * @param mixed $definition
   *   The parameter definition provided in the route options.
   * @param string $name
   *   The name of the parameter.
   *
   * @return string|null
   *   A regex pattern (without delimiters or anchors) suitable for
   *   Route::setRequirement(), or NULL if no requirement can be determined.
   */
  public function getRouteRequirement($definition, $name): ?string;

}
+13 −0
Changes for core/modules/system/tests/src/Functional/ParamConverter/UpcastingTest.php: 13 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -95,4 +95,17 @@ public function testEntityLanguage(): void {
    $this->assertSession()->pageTextContains("Deutscher Titel");
  }

  /**
   * Tests that non-ASCII config entity IDs in routes return 404.
   *
   * Config entity routes get regex requirements derived from their schema
   * constraints (e.g. machine_name pattern). Non-ASCII characters do not
   * match these patterns, so the router rejects them with a 404.
   */
  public function testNonAsciiConfigEntityRoute(): void {
    // No login needed: the router rejects the URL before access checking.
    $this->drupalGet('node/add/öüä');
    $this->assertSession()->statusCodeEquals(404);
  }

}
Loading