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- 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
98f6bd25f6094SA-CORE-2026-002 by hswww, menon, t-chen, benjifisher, cilefen, drumm, greggles, larowlan, longwave, mcdruid, ram4nd, xjm, poker10
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)) {
695037059cb4SA-CORE-2026-001 by murat_kekic, akalata, benjifisher, drumm, larowlan, mlhess, neclimdul, pandaski, poker10, ram4nd, xjm, prufloff, greggles
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,
78c9c7b7091aSA-CORE-2026-001 by murat_kekic, akalata, benjifisher, drumm, larowlan, mlhess, neclimdul, pandaski, poker10, ram4nd, xjm, prufloff, greggles
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,
439f02c7b7e1SA-CORE-2026-002 by hswww, menon, t-chen, benjifisher, cilefen, drumm, greggles, larowlan, longwave, mcdruid, ram4nd, xjm, poker10
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)) {
a057ebf74005SA-CORE-2026-002 by hswww, menon, t-chen, benjifisher, cilefen, drumm, greggles, larowlan, longwave, mcdruid, ram4nd, xjm, poker10
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)) {
81b594dd1380SA-CORE-2026-001 by murat_kekic, akalata, benjifisher, drumm, larowlan, mlhess, neclimdul, pandaski, poker10, ram4nd, xjm, prufloff, greggles
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,
8232489c2dedSA-CORE-2026-003 by cantina_security, dries, shirshaw64@gmail.com, larowlan, mcdruid, mingsong, damienmckenna, greggles, poker10, xjm
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(),
0cf26c62bc48SA-CORE-2026-002 by hswww, menon, t-chen, benjifisher, cilefen, drumm, greggles, larowlan, longwave, mcdruid, ram4nd, xjm, poker10
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)) {
8812078f73e6SA-CORE-2026-001 by murat_kekic, akalata, benjifisher, drumm, larowlan, mlhess, neclimdul, pandaski, poker10, ram4nd, xjm, prufloff, greggles
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- www.drupal.org/sa-core-2026-001nvdVendor Advisory
News mentions
1- Drupal core - Critical - Cross-site scripting - SA-CORE-2026-001Drupal Security Advisories · Apr 15, 2026