Drupal core - Critical - Cache poisoning - SA-CORE-2023-006
Description
In certain scenarios, Drupal's JSON:API module will output error backtraces. With some configurations, this may cause sensitive information to be cached and made available to anonymous users, leading to privilege escalation.
This vulnerability only affects sites with the JSON:API module enabled, and can be mitigated by uninstalling JSON:API.
The core REST and contributed GraphQL modules are not affected.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Drupal's JSON:API module can output error backtraces that, when cached, expose sensitive information to anonymous users, enabling privilege escalation.
Vulnerability
Overview In certain scenarios, the JSON:API module in Drupal core outputs error backtraces that may contain sensitive information [2]. When caching is enabled on affected sites, these error responses can be cached and served to subsequent anonymous users, leading to unintended information disclosure and potential privilege escalation. The vulnerability stems from insufficient sanitization of error output and missing cache tags to prevent caching of error responses containing backtraces [4].
Exploitation
Conditions The vulnerability only affects sites where the JSON:API module is enabled, and it requires that the site's caching mechanism is active. An attacker can trigger error conditions (e.g., malformed API requests) that cause the module to return a backtrace. The response is then cached, making the backtrace available to any anonymous user without authentication. No special privileges are needed to exploit this; only the ability to send requests to the JSON:API endpoint [3].
Impact
Successful exploitation results in the exposure of sensitive system information such as file paths, database credentials, configuration details, or other secrets present in the error backtrace. This information can be used to further compromise the Drupal site, potentially leading to full administrative access. The Drupal security team has rated this vulnerability as critical due to the risk of privilege escalation [3].
Mitigation
The Drupal project released security updates for all supported versions (9.5.11, 10.0.11, 10.1.4) that fix the issue by hiding error backtraces and adding proper cache tags [3][4]. As an immediate workaround, administrators can uninstall the JSON:API module if it is not required. The core REST and contributed GraphQL modules are not affected [2][3]. Drupal Steward WAF partners may provide partial mitigation, but updating to the patched version is strongly recommended.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
drupal/corePackagist | >= 8.7.0, < 9.5.11 | 9.5.11 |
drupal/corePackagist | >= 10.0.0, < 10.0.11 | 10.0.11 |
drupal/corePackagist | >= 10.1.0, < 10.1.4 | 10.1.4 |
Affected products
3- osv-coords2 versions
>= 8.7.0, < 9.5.11+ 1 more
- (no CPE)range: >= 8.7.0, < 9.5.11
- (no CPE)range: >= 8.7.0, < 9.5.11
- Drupal/Corev5Range: 10.1
Patches
35495dc530e3aSA-CORE-2023-006 by ghostccamm, effulgentsia, larowlan, xjm, pwolanin, catch, Wim Leers, mcdruid, benjifisher
4 files changed · +30 −3
modules/jsonapi/src/Normalizer/HttpExceptionNormalizer.php+10 −1 modified@@ -51,6 +51,12 @@ public function __construct(AccountInterface $current_user) { public function normalize($object, $format = NULL, array $context = []) { $cacheability = new CacheableMetadata(); $cacheability->addCacheableDependency($object); + + $cacheability->addCacheTags(['config:system.logging']); + if (\Drupal::config('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) { + $cacheability->setCacheMaxAge(0); + } + return new HttpExceptionNormalizerValue($cacheability, static::rasterizeValueRecursive($this->buildErrorObjects($object))); } @@ -89,7 +95,10 @@ protected function buildErrorObjects(HttpException $exception) { if ($exception->getCode() !== 0) { $error['code'] = (string) $exception->getCode(); } - if ($this->currentUser->hasPermission('access site reports')) { + + $is_verbose_reporting = \Drupal::config('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE; + $site_report_access = $this->currentUser->hasPermission('access site reports'); + if ($site_report_access && $is_verbose_reporting) { // The following information may contain sensitive information. Only show // it to authorized users. $error['source'] = [
modules/jsonapi/tests/src/Functional/ResourceTestBase.php+10 −1 modified@@ -221,6 +221,8 @@ public function setUp() { $this->serializer = $this->container->get('jsonapi.serializer'); + $this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save(); + // Ensure the anonymous user role has no permissions at all. $user_role = Role::load(RoleInterface::ANONYMOUS_ID); foreach ($user_role->getPermissions() as $permission) { @@ -725,7 +727,14 @@ protected function assertResourceResponse($expected_status_code, $expected_docum // Expected cache tags: X-Drupal-Cache-Tags header. $this->assertSame($expected_cache_tags !== FALSE, $response->hasHeader('X-Drupal-Cache-Tags')); if (is_array($expected_cache_tags)) { - $this->assertEqualsCanonicalizing($expected_cache_tags, explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0])); + $actual_cache_tags = explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0]); + + $tag = 'config:system.logging'; + if (!in_array($tag, $expected_cache_tags) && in_array($tag, $actual_cache_tags)) { + $expected_cache_tags[] = $tag; + } + + $this->assertEqualsCanonicalizing($expected_cache_tags, $actual_cache_tags); } // Expected cache contexts: X-Drupal-Cache-Contexts header.
modules/jsonapi/tests/src/Functional/RestJsonApiUnsupported.php+3 −1 modified@@ -65,6 +65,8 @@ protected function setUpAuthorization($method) { public function setUp(): void { parent::setUp(); + $this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save(); + // Create a "Camelids" node type. NodeType::create([ 'name' => 'Camelids', @@ -99,7 +101,7 @@ public function testApiJsonNotSupportedInRest() { 400, FALSE, $response, - ['4xx-response', 'config:user.role.anonymous', 'http_response', 'node:1'], + ['4xx-response', 'config:system.logging', 'config:user.role.anonymous', 'http_response', 'node:1'], ['url.query_args:_format', 'url.site', 'user.permissions'], 'MISS', 'MISS'
modules/jsonapi/tests/src/Unit/Normalizer/HttpExceptionNormalizerTest.php+7 −0 modified@@ -2,6 +2,8 @@ namespace Drupal\Tests\jsonapi\Unit\Normalizer; +use Drupal\Core\Config\ConfigFactory; +use Drupal\Core\Config\ImmutableConfig; use Drupal\Core\Session\AccountInterface; use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer; use Drupal\Tests\UnitTestCase; @@ -26,6 +28,11 @@ public function testNormalize() { $request_stack->getCurrentRequest()->willReturn(Request::create('http://localhost/')); $container = $this->prophesize(ContainerInterface::class); $container->get('request_stack')->willReturn($request_stack->reveal()); + $config = $this->prophesize(ImmutableConfig::class); + $config->get('error_level')->willReturn(ERROR_REPORTING_DISPLAY_VERBOSE); + $config_factory = $this->prophesize(ConfigFactory::class); + $config_factory->get('system.logging')->willReturn($config->reveal()); + $container->get('config.factory')->willReturn($config_factory->reveal()); \Drupal::setContainer($container->reveal()); $exception = new AccessDeniedHttpException('lorem', NULL, 13); $current_user = $this->prophesize(AccountInterface::class);
d4fe67562ee3SA-CORE-2023-006 by ghostccamm, effulgentsia, larowlan, xjm, pwolanin, catch, Wim Leers, mcdruid, benjifisher
4 files changed · +30 −3
modules/jsonapi/src/Normalizer/HttpExceptionNormalizer.php+10 −1 modified@@ -51,6 +51,12 @@ public function __construct(AccountInterface $current_user) { public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL { $cacheability = new CacheableMetadata(); $cacheability->addCacheableDependency($object); + + $cacheability->addCacheTags(['config:system.logging']); + if (\Drupal::config('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) { + $cacheability->setCacheMaxAge(0); + } + return new HttpExceptionNormalizerValue($cacheability, static::rasterizeValueRecursive($this->buildErrorObjects($object))); } @@ -89,7 +95,10 @@ protected function buildErrorObjects(HttpException $exception) { if ($exception->getCode() !== 0) { $error['code'] = (string) $exception->getCode(); } - if ($this->currentUser->hasPermission('access site reports')) { + + $is_verbose_reporting = \Drupal::config('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE; + $site_report_access = $this->currentUser->hasPermission('access site reports'); + if ($site_report_access && $is_verbose_reporting) { // The following information may contain sensitive information. Only show // it to authorized users. $error['source'] = [
modules/jsonapi/tests/src/Functional/ResourceTestBase.php+10 −1 modified@@ -221,6 +221,8 @@ protected function setUp(): void { $this->serializer = $this->container->get('jsonapi.serializer'); + $this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save(); + // Ensure the anonymous user role has no permissions at all. $user_role = Role::load(RoleInterface::ANONYMOUS_ID); foreach ($user_role->getPermissions() as $permission) { @@ -725,7 +727,14 @@ protected function assertResourceResponse($expected_status_code, $expected_docum // Expected cache tags: X-Drupal-Cache-Tags header. $this->assertSame($expected_cache_tags !== FALSE, $response->hasHeader('X-Drupal-Cache-Tags')); if (is_array($expected_cache_tags)) { - $this->assertEqualsCanonicalizing($expected_cache_tags, explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0])); + $actual_cache_tags = explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0]); + + $tag = 'config:system.logging'; + if (!in_array($tag, $expected_cache_tags) && in_array($tag, $actual_cache_tags)) { + $expected_cache_tags[] = $tag; + } + + $this->assertEqualsCanonicalizing($expected_cache_tags, $actual_cache_tags); } // Expected cache contexts: X-Drupal-Cache-Contexts header.
modules/jsonapi/tests/src/Functional/RestJsonApiUnsupported.php+3 −1 modified@@ -65,6 +65,8 @@ protected function setUpAuthorization($method) { protected function setUp(): void { parent::setUp(); + $this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save(); + // Create a "Camelids" node type. NodeType::create([ 'name' => 'Camelids', @@ -99,7 +101,7 @@ public function testApiJsonNotSupportedInRest() { 400, FALSE, $response, - ['4xx-response', 'config:user.role.anonymous', 'http_response', 'node:1'], + ['4xx-response', 'config:system.logging', 'config:user.role.anonymous', 'http_response', 'node:1'], ['url.query_args:_format', 'url.site', 'user.permissions'], 'MISS', 'MISS'
modules/jsonapi/tests/src/Unit/Normalizer/HttpExceptionNormalizerTest.php+7 −0 modified@@ -2,6 +2,8 @@ namespace Drupal\Tests\jsonapi\Unit\Normalizer; +use Drupal\Core\Config\ConfigFactory; +use Drupal\Core\Config\ImmutableConfig; use Drupal\Core\Session\AccountInterface; use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer; use Drupal\Tests\UnitTestCase; @@ -26,6 +28,11 @@ public function testNormalize() { $request_stack->getCurrentRequest()->willReturn(Request::create('http://localhost/')); $container = $this->prophesize(ContainerInterface::class); $container->get('request_stack')->willReturn($request_stack->reveal()); + $config = $this->prophesize(ImmutableConfig::class); + $config->get('error_level')->willReturn(ERROR_REPORTING_DISPLAY_VERBOSE); + $config_factory = $this->prophesize(ConfigFactory::class); + $config_factory->get('system.logging')->willReturn($config->reveal()); + $container->get('config.factory')->willReturn($config_factory->reveal()); \Drupal::setContainer($container->reveal()); $exception = new AccessDeniedHttpException('lorem', NULL, 13); $current_user = $this->prophesize(AccountInterface::class);
1cd2741c2b43SA-CORE-2023-006 by ghostccamm, effulgentsia, larowlan, xjm, pwolanin, catch, Wim Leers, mcdruid, benjifisher
4 files changed · +30 −3
modules/jsonapi/src/Normalizer/HttpExceptionNormalizer.php+10 −1 modified@@ -44,6 +44,12 @@ public function __construct(AccountInterface $current_user) { public function normalize($object, $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|NULL { $cacheability = new CacheableMetadata(); $cacheability->addCacheableDependency($object); + + $cacheability->addCacheTags(['config:system.logging']); + if (\Drupal::config('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) { + $cacheability->setCacheMaxAge(0); + } + return new HttpExceptionNormalizerValue($cacheability, static::rasterizeValueRecursive($this->buildErrorObjects($object))); } @@ -82,7 +88,10 @@ protected function buildErrorObjects(HttpException $exception) { if ($exception->getCode() !== 0) { $error['code'] = (string) $exception->getCode(); } - if ($this->currentUser->hasPermission('access site reports')) { + + $is_verbose_reporting = \Drupal::config('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE; + $site_report_access = $this->currentUser->hasPermission('access site reports'); + if ($site_report_access && $is_verbose_reporting) { // The following information may contain sensitive information. Only show // it to authorized users. $error['source'] = [
modules/jsonapi/tests/src/Functional/ResourceTestBase.php+10 −1 modified@@ -221,6 +221,8 @@ protected function setUp(): void { $this->serializer = $this->container->get('jsonapi.serializer'); + $this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save(); + // Ensure the anonymous user role has no permissions at all. $user_role = Role::load(RoleInterface::ANONYMOUS_ID); foreach ($user_role->getPermissions() as $permission) { @@ -725,7 +727,14 @@ protected function assertResourceResponse($expected_status_code, $expected_docum // Expected cache tags: X-Drupal-Cache-Tags header. $this->assertSame($expected_cache_tags !== FALSE, $response->hasHeader('X-Drupal-Cache-Tags')); if (is_array($expected_cache_tags)) { - $this->assertEqualsCanonicalizing($expected_cache_tags, explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0])); + $actual_cache_tags = explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0]); + + $tag = 'config:system.logging'; + if (!in_array($tag, $expected_cache_tags) && in_array($tag, $actual_cache_tags)) { + $expected_cache_tags[] = $tag; + } + + $this->assertEqualsCanonicalizing($expected_cache_tags, $actual_cache_tags); } // Expected cache contexts: X-Drupal-Cache-Contexts header.
modules/jsonapi/tests/src/Functional/RestJsonApiUnsupported.php+3 −1 modified@@ -65,6 +65,8 @@ protected function setUpAuthorization($method) { protected function setUp(): void { parent::setUp(); + $this->config('system.logging')->set('error_level', ERROR_REPORTING_HIDE)->save(); + // Create a "Camelids" node type. NodeType::create([ 'name' => 'Camelids', @@ -99,7 +101,7 @@ public function testApiJsonNotSupportedInRest() { 400, FALSE, $response, - ['4xx-response', 'config:user.role.anonymous', 'http_response', 'node:1'], + ['4xx-response', 'config:system.logging', 'config:user.role.anonymous', 'http_response', 'node:1'], ['url.query_args:_format', 'url.site', 'user.permissions'], 'MISS', 'MISS'
modules/jsonapi/tests/src/Unit/Normalizer/HttpExceptionNormalizerTest.php+7 −0 modified@@ -2,6 +2,8 @@ namespace Drupal\Tests\jsonapi\Unit\Normalizer; +use Drupal\Core\Config\ConfigFactory; +use Drupal\Core\Config\ImmutableConfig; use Drupal\Core\Session\AccountInterface; use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer; use Drupal\Tests\UnitTestCase; @@ -26,6 +28,11 @@ public function testNormalize() { $request_stack->getCurrentRequest()->willReturn(Request::create('http://localhost/')); $container = $this->prophesize(ContainerInterface::class); $container->get('request_stack')->willReturn($request_stack->reveal()); + $config = $this->prophesize(ImmutableConfig::class); + $config->get('error_level')->willReturn(ERROR_REPORTING_DISPLAY_VERBOSE); + $config_factory = $this->prophesize(ConfigFactory::class); + $config_factory->get('system.logging')->willReturn($config->reveal()); + $container->get('config.factory')->willReturn($config_factory->reveal()); \Drupal::setContainer($container->reveal()); $exception = new AccessDeniedHttpException('lorem', NULL, 13); $current_user = $this->prophesize(AccountInterface::class);
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-rjqg-3h9m-fx5xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-5256ghsaADVISORY
- github.com/drupal/core/commit/1cd2741c2b43f6ad1bdfc121b8d9ec3b87e70742ghsaWEB
- github.com/drupal/core/commit/5495dc530e3acd056478245bfe1828210c6da7dcghsaWEB
- github.com/drupal/core/commit/d4fe67562ee3ea0d9ecb9672d2945d94c5633d24ghsaWEB
- www.drupal.org/sa-core-2023-006ghsaWEB
News mentions
1- Drupal core - Critical - Cache poisoning - SA-CORE-2023-006Drupal Security Advisories · Sep 20, 2023