CVE-2026-6366
Description
Improperly Controlled Modification of Dynamically-Determined Object Attributes vulnerability in Drupal Drupal core allows Object Injection.
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.
Drupal core gadget chain enables object injection, requiring a separate deserialization vulnerability to achieve code execution.
Vulnerability
Drupal core versions 8.0.0 through 10.5.8, 10.6.0 through 10.6.6, 11.0.0 through 11.2.10, and 11.3.0 through 11.3.6 contain a chain of methods (a "gadget chain") that can be exploited when an insecure deserialization vulnerability exists on the site [1]. The issue is classified as an Improperly Controlled Modification of Dynamically-Determined Object Attributes vulnerability leading to Object Injection. The affected versions include all Drupal 11.1.x, 11.0.x, 10.4.x, and earlier releases which are end-of-life and no longer receive security coverage [1].
Exploitation
An attacker cannot directly exploit this vulnerability on its own. Exploitation requires a separate vulnerability that allows the attacker to pass unsafe input to the unserialize() function [1]. There are no known such vulnerabilities in Drupal core itself, but a site running third-party modules with their own deserialization flaws could provide the necessary entry point. The attacker would need the ability to control serialized data fed to unserialize() through some other means [1].
Impact
If triggered, the gadget chain enables remote code execution or SQL injection, depending on the available classes and methods [1]. The attacker could gain full control over the affected site or extract sensitive database contents. The privilege level of the compromise equals that of the user context in which the deserialization occurs, typically a privileged administrative user or the web server user.
Mitigation
Update to fixed versions: Drupal 10.5.9, 10.6.7, 11.2.11, or 11.3.7 [1]. For older branches, upgrade to a supported release. No workaround exists other than ensuring no other deserialization vulnerabilities are present on the site. Drupal 8, 9, and earlier releases are end-of-life and must be upgraded to a supported branch to receive the fix [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
13a0e6da01ecdeDrupal 10.5.9
4 files changed · +7 −11
composer.lock+4 −8 modified@@ -493,7 +493,7 @@ }, { "name": "drupal/core", - "version": "10.5.8", + "version": "10.5.9", "dist": { "type": "path", "url": "core", @@ -655,7 +655,7 @@ }, { "name": "drupal/core-project-message", - "version": "10.5.8", + "version": "10.5.9", "dist": { "type": "path", "url": "composer/Plugin/ProjectMessage", @@ -688,7 +688,7 @@ }, { "name": "drupal/core-vendor-hardening", - "version": "10.5.8", + "version": "10.5.9", "dist": { "type": "path", "url": "composer/Plugin/VendorHardening", @@ -10278,11 +10278,7 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "drupal/core": 20, - "drupal/core-project-message": 20, - "drupal/core-vendor-hardening": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": {},
composer/Metapackage/CoreRecommended/composer.json+1 −1 modified@@ -7,7 +7,7 @@ "webflo/drupal-core-strict": "*" }, "require": { - "drupal/core": "10.5.8", + "drupal/core": "10.5.9", "asm89/stack-cors": "~v2.3.0", "composer/semver": "~3.4.3", "doctrine/annotations": "~1.14.4",
composer/Metapackage/PinnedDevDependencies/composer.json+1 −1 modified@@ -7,7 +7,7 @@ "webflo/drupal-core-require-dev": "*" }, "require": { - "drupal/core": "10.5.8", + "drupal/core": "10.5.9", "behat/mink": "v1.12.0", "behat/mink-browserkit-driver": "v2.2.0", "brick/math": "0.12.3",
core/lib/Drupal.php+1 −1 modified@@ -75,7 +75,7 @@ class Drupal { /** * The current system version. */ - const VERSION = '10.5.8'; + const VERSION = '10.5.9'; /** * Core API compatibility.
8f6bd25f6094SA-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,
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)) {
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,
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,
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)) {
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(),
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,
02c7b7e1e83ca057ebfded321c85a9e06fa9Issue #3584774 by longwave, xjm: Update to Composer 2.9.7
2 files changed · +7 −7
composer.lock+6 −6 modified@@ -4827,16 +4827,16 @@ }, { "name": "composer/composer", - "version": "2.9.3", + "version": "2.9.7", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "fb3bee27676fd852a8a11ebbb1de19b4dada5aba" + "reference": "82a2fbd1372a98d7915cfb092acf05207d9b4113" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/fb3bee27676fd852a8a11ebbb1de19b4dada5aba", - "reference": "fb3bee27676fd852a8a11ebbb1de19b4dada5aba", + "url": "https://api.github.com/repos/composer/composer/zipball/82a2fbd1372a98d7915cfb092acf05207d9b4113", + "reference": "82a2fbd1372a98d7915cfb092acf05207d9b4113", "shasum": "" }, "require": { @@ -4924,7 +4924,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.9.3" + "source": "https://github.com/composer/composer/tree/2.9.7" }, "funding": [ { @@ -4936,7 +4936,7 @@ "type": "github" } ], - "time": "2025-12-30T12:40:17+00:00" + "time": "2026-04-14T11:31:52+00:00" }, { "name": "composer/metadata-minifier",
composer/Metapackage/PinnedDevDependencies/composer.json+1 −1 modified@@ -14,7 +14,7 @@ "colinodell/psr-testlogger": "v1.3.1", "composer/ca-bundle": "1.5.9", "composer/class-map-generator": "1.7.0", - "composer/composer": "2.9.3", + "composer/composer": "2.9.7", "composer/metadata-minifier": "1.0.0", "composer/pcre": "3.3.2", "composer/spdx-licenses": "1.5.9",
Vulnerability mechanics
Root cause
"Unrestricted deserialization in ViewExecutable::__wakeup() allows an attacker with high privileges to inject arbitrary object properties, and unsanitized button attributes in OpenDialogCommand::render() allow XSS-based object manipulation."
Attack vector
An attacker with high privileges (e.g., a content administrator) can craft a malicious serialized ViewExecutable object where the `serializationData` array contains unexpected keys or where other properties differ from their defaults. When the object is unserialized, the `__wakeup()` method (prior to the patch) did not validate the structure, enabling arbitrary property injection [CWE-915]. Additionally, in the AJAX OpenDialogCommand, the `buttons` array in `dialogOptions` was not sanitized; an attacker could inject arbitrary HTML attributes or XSS payloads through button values, which are rendered unsafely [CWE-915]. Both vectors require an authenticated user with the ability to create or manipulate serialized data or AJAX command configurations.
Affected code
The primary vulnerable code is in `core/modules/views/src/ViewExecutable.php` in the `__wakeup()` method, which previously performed no validation on deserialized properties. The second vulnerable location is `core/lib/Drupal/Core/Ajax/OpenDialogCommand.php` in the `render()` method, where the `buttons` array within `dialogOptions` was rendered without sanitization. A third, less severe issue is in `core/modules/ckeditor5/src/Controller/EntityLinkSuggestionsController.php` where the entity label was not escaped.
What the fix does
The ViewExecutable patch [patch_id=895524, patch_id=897830] adds a `__wakeup()` validation loop that uses reflection to compare every property against its default value; any property that differs from the default (except `serializationData`) causes a `RuntimeException`. For `serializationData`, the patch enforces an exact set of allowed keys, rejecting any unexpected keys. The OpenDialogCommand patch [patch_id=895523, patch_id=897832] sanitizes button attributes by whitelisting only `disabled`, `icons`, `label`, and `text`, and applies `Xss::filter()` to all string values. The EntityLinkSuggestionsController patch [patch_id=897831] escapes the entity label with `Html::escape()` to prevent XSS in CKEditor 5 link suggestions.
Preconditions
- authAttacker must have a Drupal account with high privileges (e.g., administrator or content administrator) capable of creating or manipulating serialized ViewExecutable objects or AJAX command configurations.
- inputFor the deserialization vector, the attacker must supply a crafted serialized object. For the AJAX vector, the attacker must control the `dialogOptions['buttons']` array.
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-002nvdVendor Advisory
News mentions
1- Drupal core - Moderately critical - Gadget Chain - SA-CORE-2026-002Drupal Security Advisories · Apr 15, 2026