VYPR
Medium severity6.1NVD Advisory· Published May 19, 2026· Updated May 20, 2026

CVE-2026-6365

CVE-2026-6365

Description

Improper Neutralization of Input During Web Page Generation ("Cross-site Scripting") vulnerability in Drupal Drupal core allows Cross-Site Scripting (XSS).

This issue affects Drupal core: from 8.0.0 before 10.5.9, from 10.6.0 before 10.6.7, from 11.0.0 before 11.2.11, from 11.3.0 before 11.3.7.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Improper sanitization of AJAX modal dialog options in Drupal core allows cross-site scripting (XSS) in multiple versions.

Vulnerability

Drupal core's jQuery integration for AJAX modal dialog boxes does not properly sanitize certain options, leading to a cross-site scripting (XSS) vulnerability. This affects Drupal core versions: from 8.0.0 before 10.5.9, from 10.6.0 before 10.6.7, from 11.0.0 before 11.2.11, and from 11.3.0 before 11.3.7. Versions 11.1.x, 11.0.x, 10.4.x and below are end-of-life and do not receive security coverage [1].

Exploitation

An attacker can exploit this by providing crafted input that influences the options passed to the AJAX modal dialog. When a user with sufficient permissions (e.g., administrator) triggers the modal, the injected script executes in the context of their session. The vulnerability does not require authentication beyond the ability to supply the malicious input, but user interaction (e.g., clicking a link or visiting a page) is needed to trigger the dialog [1].

Impact

Successful exploitation allows an attacker to execute arbitrary JavaScript in the victim's browser, potentially leading to session hijacking, data theft, or defacement. The scope may change as the attacker can perform actions on behalf of the victim within the Drupal site, possibly affecting other users or administrative functions [1].

Mitigation

Users should upgrade to the latest fixed versions: Drupal 10.5.9, 10.6.7, 11.2.11, or 11.3.7, depending on the branch in use. For end-of-life versions (10.4.x and earlier, including Drupal 8 and 9), no fix is available; upgrading to a supported branch is required. No workarounds have been disclosed [1].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Drupal/Drupalinferred
    Range: >=8.0.0,<10.5.9 or >=10.6.0,<10.6.7 or >=11.0.0,<11.2.11 or >=11.3.0,<11.3.7
  • Range: >=8.0.0, <10.5.9 || >=10.6.0, <10.6.7 || >=11.0.0, <11.2.11 || >=11.3.0, <11.3.7

Patches

9
8f6bd25f6094

SA-CORE-2026-002 by hswww, menon, t-chen, benjifisher, cilefen, drumm, greggles, larowlan, longwave, mcdruid, ram4nd, xjm, poker10

https://github.com/drupal/drupalxjmApr 15, 2026Fixed in 10.5.9via llm-release-walk
2 files changed · +30 0
  • core/lib/Drupal/Core/Template/Attribute.php+3 0 modified
    @@ -330,6 +330,9 @@ public function __toString() {
         $return = '';
         /** @var \Drupal\Core\Template\AttributeValueBase $value */
         foreach ($this->storage as $value) {
    +      if (!$value instanceof AttributeValueBase) {
    +        throw new \RuntimeException(sprintf('Unexpected type for $value (%s).', get_debug_type($value)));
    +      }
           $rendered = $value->render();
           if ($rendered) {
             $return .= ' ' . $rendered;
    
  • core/modules/views/src/ViewExecutable.php+27 0 modified
    @@ -2557,6 +2557,33 @@ public function __sleep() {
        * Magic method implementation to unserialize the view executable.
        */
       public function __wakeup() {
    +    $reflection = new \ReflectionClass($this);
    +    $defaults = $reflection->getDefaultProperties();
    +    foreach ($reflection->getProperties() as $property) {
    +      $name = $property->getName();
    +      if ($name === 'serializationData') {
    +        $expected_keys = [
    +          'args',
    +          'current_display',
    +          'current_page',
    +          'dom_id',
    +          'executed',
    +          'exposed_data',
    +          'exposed_input',
    +          'exposed_raw_input',
    +          'storage',
    +        ];
    +        $actual_keys = array_keys($this->serializationData);
    +        sort($actual_keys);
    +        if ($actual_keys !== $expected_keys) {
    +          throw new \RuntimeException(sprintf('Unexpected keys in %s::$%s.', __CLASS__, $name));
    +        }
    +      }
    +      elseif (array_key_exists($name, $defaults) && $this->$name !== $defaults[$name]) {
    +        throw new \RuntimeException(sprintf('Deserialization of %s::$%s is not allowed.', __CLASS__, $name));
    +      }
    +    }
    +
         // There are cases, like in testing where we don't have a container
         // available.
         if (\Drupal::hasContainer() && !empty($this->serializationData)) {
    
695037059cb4

SA-CORE-2026-001 by murat_kekic, akalata, benjifisher, drumm, larowlan, mlhess, neclimdul, pandaski, poker10, ram4nd, xjm, prufloff, greggles

https://github.com/drupal/drupalxjmApr 15, 2026Fixed in 10.5.9via llm-release-walk
1 file changed · +15 0
  • core/lib/Drupal/Core/Ajax/OpenDialogCommand.php+15 0 modified
    @@ -3,6 +3,7 @@
     namespace Drupal\Core\Ajax;
     
     use Drupal\Component\Render\PlainTextOutput;
    +use Drupal\Component\Utility\Xss;
     
     /**
      * Defines an AJAX command to open certain content in a dialog.
    @@ -141,6 +142,20 @@ public function setDialogTitle($title) {
       public function render() {
         // For consistency ensure the modal option is set to TRUE or FALSE.
         $this->dialogOptions['modal'] = isset($this->dialogOptions['modal']) && $this->dialogOptions['modal'];
    +
    +    if (!empty($this->dialogOptions['buttons'])) {
    +      foreach ($this->dialogOptions['buttons'] as &$button) {
    +        // Only allow specific attributes to be defined for a button.
    +        $button = \array_intersect_key($button, \array_flip(['disabled', 'icons', 'label', 'text']));
    +        foreach ($button as &$value) {
    +          if (is_string($value)) {
    +            // Apply Xss::filter to button attribute values.
    +            $value = Xss::filter($value);
    +          }
    +        }
    +      }
    +    }
    +
         return [
           'command' => 'openDialog',
           'selector' => $this->selector,
    
78c9c7b7091a

SA-CORE-2026-001 by murat_kekic, akalata, benjifisher, drumm, larowlan, mlhess, neclimdul, pandaski, poker10, ram4nd, xjm, prufloff, greggles

https://github.com/drupal/drupalxjmApr 15, 2026Fixed in 10.6.7via llm-release-walk
1 file changed · +15 0
  • core/lib/Drupal/Core/Ajax/OpenDialogCommand.php+15 0 modified
    @@ -3,6 +3,7 @@
     namespace Drupal\Core\Ajax;
     
     use Drupal\Component\Render\PlainTextOutput;
    +use Drupal\Component\Utility\Xss;
     
     /**
      * Defines an AJAX command to open certain content in a dialog.
    @@ -141,6 +142,20 @@ public function setDialogTitle($title) {
       public function render() {
         // For consistency ensure the modal option is set to TRUE or FALSE.
         $this->dialogOptions['modal'] = isset($this->dialogOptions['modal']) && $this->dialogOptions['modal'];
    +
    +    if (!empty($this->dialogOptions['buttons'])) {
    +      foreach ($this->dialogOptions['buttons'] as &$button) {
    +        // Only allow specific attributes to be defined for a button.
    +        $button = \array_intersect_key($button, \array_flip(['disabled', 'icons', 'label', 'text']));
    +        foreach ($button as &$value) {
    +          if (is_string($value)) {
    +            // Apply Xss::filter to button attribute values.
    +            $value = Xss::filter($value);
    +          }
    +        }
    +      }
    +    }
    +
         return [
           'command' => 'openDialog',
           'selector' => $this->selector,
    
439f02c7b7e1

SA-CORE-2026-002 by hswww, menon, t-chen, benjifisher, cilefen, drumm, greggles, larowlan, longwave, mcdruid, ram4nd, xjm, poker10

https://github.com/drupal/drupalxjmApr 15, 2026Fixed in 10.6.7via llm-release-walk
2 files changed · +30 0
  • core/lib/Drupal/Core/Template/Attribute.php+3 0 modified
    @@ -330,6 +330,9 @@ public function __toString() {
         $return = '';
         /** @var \Drupal\Core\Template\AttributeValueBase $value */
         foreach ($this->storage as $value) {
    +      if (!$value instanceof AttributeValueBase) {
    +        throw new \RuntimeException(sprintf('Unexpected type for $value (%s).', get_debug_type($value)));
    +      }
           $rendered = $value->render();
           if ($rendered) {
             $return .= ' ' . $rendered;
    
  • core/modules/views/src/ViewExecutable.php+27 0 modified
    @@ -2557,6 +2557,33 @@ public function __sleep() {
        * Magic method implementation to unserialize the view executable.
        */
       public function __wakeup() {
    +    $reflection = new \ReflectionClass($this);
    +    $defaults = $reflection->getDefaultProperties();
    +    foreach ($reflection->getProperties() as $property) {
    +      $name = $property->getName();
    +      if ($name === 'serializationData') {
    +        $expected_keys = [
    +          'args',
    +          'current_display',
    +          'current_page',
    +          'dom_id',
    +          'executed',
    +          'exposed_data',
    +          'exposed_input',
    +          'exposed_raw_input',
    +          'storage',
    +        ];
    +        $actual_keys = array_keys($this->serializationData);
    +        sort($actual_keys);
    +        if ($actual_keys !== $expected_keys) {
    +          throw new \RuntimeException(sprintf('Unexpected keys in %s::$%s.', __CLASS__, $name));
    +        }
    +      }
    +      elseif (array_key_exists($name, $defaults) && $this->$name !== $defaults[$name]) {
    +        throw new \RuntimeException(sprintf('Deserialization of %s::$%s is not allowed.', __CLASS__, $name));
    +      }
    +    }
    +
         // There are cases, like in testing where we don't have a container
         // available.
         if (\Drupal::hasContainer() && !empty($this->serializationData)) {
    
a057ebf74005

SA-CORE-2026-002 by hswww, menon, t-chen, benjifisher, cilefen, drumm, greggles, larowlan, longwave, mcdruid, ram4nd, xjm, poker10

https://github.com/drupal/drupalxjmApr 15, 2026Fixed in 11.2.11via llm-release-walk
2 files changed · +30 0
  • core/lib/Drupal/Core/Template/Attribute.php+3 0 modified
    @@ -326,6 +326,9 @@ public function __toString() {
         $return = '';
         /** @var \Drupal\Core\Template\AttributeValueBase $value */
         foreach ($this->storage as $value) {
    +      if (!$value instanceof AttributeValueBase) {
    +        throw new \RuntimeException(sprintf('Unexpected type for $value (%s).', get_debug_type($value)));
    +      }
           $rendered = $value->render();
           if ($rendered) {
             $return .= ' ' . $rendered;
    
  • core/modules/views/src/ViewExecutable.php+27 0 modified
    @@ -2559,6 +2559,33 @@ public function __sleep(): array {
        * Magic method implementation to unserialize the view executable.
        */
       public function __wakeup(): void {
    +    $reflection = new \ReflectionClass($this);
    +    $defaults = $reflection->getDefaultProperties();
    +    foreach ($reflection->getProperties() as $property) {
    +      $name = $property->getName();
    +      if ($name === 'serializationData') {
    +        $expected_keys = [
    +          'args',
    +          'current_display',
    +          'current_page',
    +          'dom_id',
    +          'executed',
    +          'exposed_data',
    +          'exposed_input',
    +          'exposed_raw_input',
    +          'storage',
    +        ];
    +        $actual_keys = array_keys($this->serializationData);
    +        sort($actual_keys);
    +        if ($actual_keys !== $expected_keys) {
    +          throw new \RuntimeException(sprintf('Unexpected keys in %s::$%s.', __CLASS__, $name));
    +        }
    +      }
    +      elseif (array_key_exists($name, $defaults) && $this->$name !== $defaults[$name]) {
    +        throw new \RuntimeException(sprintf('Deserialization of %s::$%s is not allowed.', __CLASS__, $name));
    +      }
    +    }
    +
         // There are cases, like in testing where we don't have a container
         // available.
         if (\Drupal::hasContainer() && !empty($this->serializationData)) {
    
81b594dd1380

SA-CORE-2026-001 by murat_kekic, akalata, benjifisher, drumm, larowlan, mlhess, neclimdul, pandaski, poker10, ram4nd, xjm, prufloff, greggles

https://github.com/drupal/drupalxjmApr 15, 2026Fixed in 11.2.11via llm-release-walk
1 file changed · +15 0
  • core/lib/Drupal/Core/Ajax/OpenDialogCommand.php+15 0 modified
    @@ -3,6 +3,7 @@
     namespace Drupal\Core\Ajax;
     
     use Drupal\Component\Render\PlainTextOutput;
    +use Drupal\Component\Utility\Xss;
     
     /**
      * Defines an AJAX command to open certain content in a dialog.
    @@ -139,6 +140,20 @@ public function setDialogTitle($title) {
       public function render() {
         // For consistency ensure the modal option is set to TRUE or FALSE.
         $this->dialogOptions['modal'] = isset($this->dialogOptions['modal']) && $this->dialogOptions['modal'];
    +
    +    if (!empty($this->dialogOptions['buttons'])) {
    +      foreach ($this->dialogOptions['buttons'] as &$button) {
    +        // Only allow specific attributes to be defined for a button.
    +        $button = \array_intersect_key($button, \array_flip(['disabled', 'icons', 'label', 'text']));
    +        foreach ($button as &$value) {
    +          if (is_string($value)) {
    +            // Apply Xss::filter to button attribute values.
    +            $value = Xss::filter($value);
    +          }
    +        }
    +      }
    +    }
    +
         return [
           'command' => 'openDialog',
           'selector' => $this->selector,
    
8232489c2ded

SA-CORE-2026-003 by cantina_security, dries, shirshaw64@gmail.com, larowlan, mcdruid, mingsong, damienmckenna, greggles, poker10, xjm

https://github.com/drupal/drupalxjmApr 15, 2026Fixed in 11.3.7via llm-release-walk
1 file changed · +1 1
  • core/modules/ckeditor5/src/Controller/EntityLinkSuggestionsController.php+1 1 modified
    @@ -233,7 +233,7 @@ protected function createSuggestion(EntityInterface $entity): array {
           'entity_type_id' => $entity->getEntityTypeId(),
           'entity_uuid' => $entity->uuid(),
           'group' => $this->computeGroup($entity),
    -      'label' => $entity->label(),
    +      'label' => Html::escape($entity->label() ?? ''),
           // Use the canonical URI as a valid fallback for the href. The
           // text_format filter will transform this to the final URL (e.g., alias).
           'path' => $entity->toUrl('canonical')->toString(),
    
0cf26c62bc48

SA-CORE-2026-002 by hswww, menon, t-chen, benjifisher, cilefen, drumm, greggles, larowlan, longwave, mcdruid, ram4nd, xjm, poker10

https://github.com/drupal/drupalxjmApr 15, 2026Fixed in 11.3.7via llm-release-walk
2 files changed · +30 0
  • core/lib/Drupal/Core/Template/Attribute.php+3 0 modified
    @@ -329,6 +329,9 @@ public function __toString() {
         $return = '';
         /** @var \Drupal\Core\Template\AttributeValueBase $value */
         foreach ($this->storage as $value) {
    +      if (!$value instanceof AttributeValueBase) {
    +        throw new \RuntimeException(sprintf('Unexpected type for $value (%s).', get_debug_type($value)));
    +      }
           $rendered = $value->render();
           if ($rendered) {
             $return .= ' ' . $rendered;
    
  • core/modules/views/src/ViewExecutable.php+27 0 modified
    @@ -2553,6 +2553,33 @@ public function __sleep(): array {
        * Magic method implementation to unserialize the view executable.
        */
       public function __wakeup(): void {
    +    $reflection = new \ReflectionClass($this);
    +    $defaults = $reflection->getDefaultProperties();
    +    foreach ($reflection->getProperties() as $property) {
    +      $name = $property->getName();
    +      if ($name === 'serializationData') {
    +        $expected_keys = [
    +          'args',
    +          'current_display',
    +          'current_page',
    +          'dom_id',
    +          'executed',
    +          'exposed_data',
    +          'exposed_input',
    +          'exposed_raw_input',
    +          'storage',
    +        ];
    +        $actual_keys = array_keys($this->serializationData);
    +        sort($actual_keys);
    +        if ($actual_keys !== $expected_keys) {
    +          throw new \RuntimeException(sprintf('Unexpected keys in %s::$%s.', __CLASS__, $name));
    +        }
    +      }
    +      elseif (array_key_exists($name, $defaults) && $this->$name !== $defaults[$name]) {
    +        throw new \RuntimeException(sprintf('Deserialization of %s::$%s is not allowed.', __CLASS__, $name));
    +      }
    +    }
    +
         // There are cases, like in testing where we don't have a container
         // available.
         if (\Drupal::hasContainer() && !empty($this->serializationData)) {
    
8812078f73e6

SA-CORE-2026-001 by murat_kekic, akalata, benjifisher, drumm, larowlan, mlhess, neclimdul, pandaski, poker10, ram4nd, xjm, prufloff, greggles

https://github.com/drupal/drupalxjmApr 15, 2026Fixed in 11.3.7via llm-release-walk
1 file changed · +15 0
  • core/lib/Drupal/Core/Ajax/OpenDialogCommand.php+15 0 modified
    @@ -3,6 +3,7 @@
     namespace Drupal\Core\Ajax;
     
     use Drupal\Component\Render\PlainTextOutput;
    +use Drupal\Component\Utility\Xss;
     
     /**
      * Defines an AJAX command to open certain content in a dialog.
    @@ -146,6 +147,20 @@ public function setDialogTitle($title) {
       public function render() {
         // For consistency ensure the modal option is set to TRUE or FALSE.
         $this->dialogOptions['modal'] = isset($this->dialogOptions['modal']) && $this->dialogOptions['modal'];
    +
    +    if (!empty($this->dialogOptions['buttons'])) {
    +      foreach ($this->dialogOptions['buttons'] as &$button) {
    +        // Only allow specific attributes to be defined for a button.
    +        $button = \array_intersect_key($button, \array_flip(['disabled', 'icons', 'label', 'text']));
    +        foreach ($button as &$value) {
    +          if (is_string($value)) {
    +            // Apply Xss::filter to button attribute values.
    +            $value = Xss::filter($value);
    +          }
    +        }
    +      }
    +    }
    +
         return [
           'command' => 'openDialog',
           'selector' => $this->selector,
    

Vulnerability mechanics

Root cause

"Insufficient deserialization validation in ViewExecutable::__wakeup() and missing type check in Attribute::__toString() allow an attacker to inject arbitrary objects during PHP unserialization, leading to cross-site scripting."

Attack vector

An unauthenticated attacker can trigger this vulnerability by delivering a crafted serialized PHP payload to a Drupal site that deserializes untrusted data. The __wakeup() method in ViewExecutable [patch_id=898057] previously did not validate that only expected properties were restored, and Attribute::__toString() [patch_id=898057] did not verify that stored values were instances of AttributeValueBase. By supplying a malicious serialized object, an attacker can inject arbitrary HTML or JavaScript into rendered page output, achieving stored or reflected cross-site scripting (XSS) [CWE-79]. The attack requires no authentication and is exploitable over the network via any vector that feeds untrusted serialized data into the affected code paths.

Affected code

The vulnerability exists in core/modules/views/src/ViewExecutable.php in the __wakeup() magic method, and in core/lib/Drupal/Core/Template/Attribute.php in the __toString() method. The ViewExecutable::__wakeup() method lacked validation of deserialized property values, and Attribute::__toString() lacked a type check on elements in the $this->storage array.

What the fix does

The patch adds two defensive checks. In ViewExecutable::__wakeup() [patch_id=898057], a new validation loop uses reflection to compare the actual keys of $this->serializationData against a whitelist of expected keys and throws a RuntimeException if any unexpected key is present. It also rejects deserialization of any property whose value differs from its class default, preventing injection of arbitrary object state. In Attribute::__toString() [patch_id=898057], a type check ensures that each element in $this->storage is an instance of AttributeValueBase before rendering; non-conforming values throw an exception. Together these changes block the injection of malicious objects or strings that could lead to XSS.

Preconditions

  • inputThe attacker must be able to supply a serialized PHP payload that reaches the affected deserialization code paths (ViewExecutable::__wakeup or Attribute::__toString).
  • authNo authentication is required (CVSS PR:N).
  • networkThe attack is exploitable over the network (CVSS AV:N).

Generated on May 20, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

1

News mentions

1