Medium severity5.3NVD Advisory· Published Apr 3, 2025· Updated Apr 15, 2026
CVE-2023-47639
CVE-2023-47639
Description
API Platform Core is a system to create hypermedia-driven REST and GraphQL APIs. From 3.2.0 until 3.2.4, exception messages, that are not HTTP exceptions, are visible in the JSON error response. This vulnerability is fixed in 3.2.5.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
api-platform/corePackagist | >= 3.2.0, < 3.2.5 | 3.2.5 |
Patches
273569fc70e01ba8a7e6538bcfix: exception message leak
9 files changed · +104 −27
src/ApiResource/Error.php+28 −9 modified@@ -58,13 +58,14 @@ class Error extends \Exception implements ProblemExceptionInterface, HttpExceptionInterface { public function __construct( - private readonly string $title, - private readonly string $detail, + private string $title, + private string $detail, #[ApiProperty(identifier: true)] private int $status, array $originalTrace = null, private ?string $instance = null, private string $type = 'about:blank', - private array $headers = [] + private array $headers = [], + private ?\Exception $previous = null ) { parent::__construct(); @@ -85,21 +86,21 @@ public function __construct( #[SerializedName('hydra:title')] #[Groups(['jsonld', 'legacy_jsonld'])] - public function getHydraTitle(): string + public function getHydraTitle(): ?string { return $this->title; } #[SerializedName('hydra:description')] #[Groups(['jsonld', 'legacy_jsonld'])] - public function getHydraDescription(): string + public function getHydraDescription(): ?string { return $this->detail; } #[SerializedName('description')] #[Groups(['jsonapi', 'legacy_jsonapi'])] - public function getDescription(): string + public function getDescription(): ?string { return $this->detail; } @@ -108,7 +109,7 @@ public static function createFromException(\Exception|\Throwable $exception, int { $headers = ($exception instanceof SymfonyHttpExceptionInterface || $exception instanceof HttpExceptionInterface) ? $exception->getHeaders() : []; - return new self('An error occurred', $exception->getMessage(), $status, $exception->getTrace(), type: '/errors/'.$status, headers: $headers); + return new self('An error occurred', $exception->getMessage(), $status, $exception->getTrace(), type: '/errors/'.$status, headers: $headers, previous: $exception->getPrevious()); } #[Ignore] @@ -123,6 +124,9 @@ public function getStatusCode(): int return $this->status; } + /** + * @param array<string, string> $headers + */ public function setHeaders(array $headers): void { $this->headers = $headers; @@ -134,15 +138,20 @@ public function getType(): string return $this->type; } + public function setType(string $type): void + { + $this->type = $type; + } + #[Groups(['jsonld', 'legacy_jsonproblem', 'jsonproblem', 'jsonapi', 'legacy_jsonapi'])] public function getTitle(): ?string { return $this->title; } - public function setType(string $type): void + public function setTitle(string $title = null): void { - $this->type = $type; + $this->title = $title; } #[Groups(['jsonld', 'jsonproblem', 'legacy_jsonproblem'])] @@ -162,9 +171,19 @@ public function getDetail(): ?string return $this->detail; } + public function setDetail(string $detail = null): void + { + $this->detail = $detail; + } + #[Groups(['jsonld', 'jsonproblem', 'legacy_jsonproblem'])] public function getInstance(): ?string { return $this->instance; } + + public function setInstance(string $instance = null): void + { + $this->instance = $instance; + } }
src/Hydra/Serializer/ErrorNormalizer.php+1 −3 modified@@ -24,7 +24,7 @@ /** * Converts {@see \Exception} or {@see FlattenException} to a Hydra error representation. * - * @deprecated we use ItemNormalizer instead + * @deprecated we will use the ItemNormalizer in 4.x instead * * @author Kévin Dunglas <dunglas@gmail.com> * @author Samuel ROZE <samuel.roze@gmail.com> @@ -47,8 +47,6 @@ public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGene */ public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { - trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource. We fallback on "api_platform.serializer.normalizer.item".', __CLASS__)); - if ($this->itemNormalizer) { return $this->itemNormalizer->normalize($object, $format, $context); }
src/JsonApi/Serializer/ErrorNormalizer.php+1 −3 modified@@ -22,7 +22,7 @@ /** * Converts {@see \Exception} or {@see FlattenException} or to a JSON API error representation. * - * @deprecated we use ItemNormalizer instead + * @deprecated we will use the ItemNormalizer in 4.x instead * * @author Héctor Hurtarte <hectorh30@gmail.com> */ @@ -46,8 +46,6 @@ public function __construct(private readonly bool $debug = false, array $default */ public function normalize(mixed $object, string $format = null, array $context = []): array { - trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource. We fallback on "api_platform.serializer.normalizer.item".', __CLASS__)); - if ($this->itemNormalizer) { return $this->itemNormalizer->normalize($object, $format, $context); }
src/Problem/Serializer/ErrorNormalizer.php+1 −3 modified@@ -22,7 +22,7 @@ * Normalizes errors according to the API Problem spec (RFC 7807). * * @see https://tools.ietf.org/html/rfc7807 - * @deprecated we use ItemNormalizer instead + * @deprecated we will use the ItemNormalizer in 4.x instead * * @author Kévin Dunglas <dunglas@gmail.com> */ @@ -47,8 +47,6 @@ public function __construct(private readonly bool $debug = false, array $default */ public function normalize(mixed $object, string $format = null, array $context = []): array { - trigger_deprecation('api-platform', '3.2', sprintf('The class "%s" is deprecated in favor of using an Error resource. We fallback on "api_platform.serializer.normalizer.item".', __CLASS__)); - if ($this->itemNormalizer) { return $this->itemNormalizer->normalize($object, $format, $context); }
src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php+1 −0 modified@@ -251,6 +251,7 @@ private function registerCommonConfiguration(ContainerBuilder $container, array } $container->setParameter('api_platform.asset_package', $config['asset_package']); $container->setParameter('api_platform.defaults', $this->normalizeDefaults($config['defaults'] ?? [])); + $container->setParameter('api_platform.rfc_7807_compliant_errors', $config['defaults']['extra_properties']['rfc_7807_compliant_errors'] ?? false); if ($container->getParameter('kernel.debug')) { $container->removeDefinition('api_platform.serializer.mapping.cache_class_metadata_factory');
src/Symfony/Bundle/Resources/config/api.xml+1 −0 modified@@ -198,6 +198,7 @@ <argument key="$exceptionToStatus">%api_platform.exception_to_status%</argument> <argument key="$identifiersExtractor" type="service" id="api_platform.api.identifiers_extractor"/> <argument key="$resourceClassResolver" type="service" id="api_platform.resource_class_resolver"/> + <argument key="$problemCompliantErrors">%api_platform.rfc_7807_compliant_errors%</argument> </service> </services> </container>
src/Symfony/EventListener/ErrorListener.php+23 −8 modified@@ -57,7 +57,8 @@ public function __construct( private readonly array $exceptionToStatus = [], private readonly null|IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface $identifiersExtractor = null, private readonly null|ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver = null, - Negotiator $negotiator = null + Negotiator $negotiator = null, + private readonly bool $problemCompliantErrors = true ) { parent::__construct($controller, $logger, $debug, $exceptionsMapping); $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; @@ -66,16 +67,31 @@ public function __construct( protected function duplicateRequest(\Throwable $exception, Request $request): Request { - $dup = parent::duplicateRequest($exception, $request); - $apiOperation = $this->initializeOperation($request); + if (false === $this->problemCompliantErrors) { + // TODO: deprecate in API Platform 3.3 + $this->controller = 'api_platform.action.exception'; + $dup = parent::duplicateRequest($exception, $request); + $dup->attributes->set('_api_operation', $apiOperation); + + return $dup; + } if ($this->debug) { $this->logger?->error('An exception occured, transforming to an Error resource.', ['exception' => $exception, 'operation' => $apiOperation]); } $format = $this->getRequestFormat($request, $this->errorFormats, false); + // Let the error handler take this we don't handle HTML + if ('html' === $format) { + $this->controller = 'error_controller'; + $dup = parent::duplicateRequest($exception, $request); + + return $dup; + } + + $dup = parent::duplicateRequest($exception, $request); if ($this->resourceMetadataCollectionFactory) { if ($this->resourceClassResolver?->isResourceClass($exception::class)) { $resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class); @@ -126,9 +142,8 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re $operation = $operation->withProvider([self::class, 'provide']); } - // For our swagger Ui errors - if ('html' === $format) { - $operation = $operation->withOutputFormats(['html' => ['text/html']]); + if (!$this->debug && $operation->getStatus() >= 500) { + $errorResource->setDetail('Internal Server Error'); } $identifiers = []; @@ -144,7 +159,7 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re ]); } - if ('jsonld' === $format && !($apiOperation?->getExtraProperties()['rfc_7807_compliant_errors'] ?? false)) { + if ($apiOperation && 'jsonld' === $format && !($apiOperation?->getExtraProperties()['rfc_7807_compliant_errors'] ?? false)) { $operation = $operation->withOutputFormats(['jsonld' => ['application/ld+json']]) ->withLinks([]) ->withExtraProperties(['rfc_7807_compliant_errors' => false] + $operation->getExtraProperties()); @@ -154,7 +169,7 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re $dup->attributes->set('_api_previous_operation', $apiOperation); $dup->attributes->set('_api_operation', $operation); $dup->attributes->set('_api_operation_name', $operation->getName()); - $dup->attributes->set('exception', $errorResource); + $dup->attributes->set('exception', $exception); // These are for swagger $dup->attributes->set('_api_original_route', $request->attributes->get('_route')); $dup->attributes->set('_api_original_route_params', $request->attributes->get('_route_params'));
tests/Symfony/Bundle/Test/ApiTestCaseTest.php+1 −1 modified@@ -290,7 +290,7 @@ private function recreateSchema(array $options = []): void */ public function testExceptionNormalizer(): void { - $this->expectDeprecation('Since api-platform 3.2: The class "ApiPlatform\Problem\Serializer\ErrorNormalizer" is deprecated in favor of using an Error resource. We fallback on "api_platform.serializer.normalizer.item".'); + // $this->expectDeprecation('Since api-platform 3.2: The class "ApiPlatform\Problem\Serializer\ErrorNormalizer" is deprecated in favor of using an Error resource. We fallback on "api_platform.serializer.normalizer.item".'); $response = self::createClient()->request('GET', '/issue5921', [ 'headers' => [
tests/Symfony/EventListener/ErrorListenerTest.php+47 −0 modified@@ -108,4 +108,51 @@ public function testDuplicateExceptionWithErrorResource(): void $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory->reveal(), ['jsonld' => ['application/ld+json']], [], $identifiersExtractor->reveal(), $resourceClassResolver->reveal()); $errorListener->onKernelException($exceptionEvent); } + + public function testDisableErrorResourceHandling(): void + { + $exception = Error::createFromException(new \Exception(), 400); + $operation = new Get(name: '_api_errors_hydra', priority: 0, status: 400, outputFormats: ['jsonld' => ['application/ld+json']]); + $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); + $kernel = $this->prophesize(KernelInterface::class); + $kernel->handle(Argument::that(function ($request) { + $this->assertEquals($request->attributes->get('_api_operation'), null); + + return true; + }), HttpKernelInterface::SUB_REQUEST, false)->willReturn(new Response()); + $exceptionEvent = new ExceptionEvent($kernel->reveal(), Request::create('/'), HttpKernelInterface::SUB_REQUEST, $exception); + $identifiersExtractor = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractor->getIdentifiersFromItem($exception, Argument::any())->willReturn(['id' => 1]); + $errorListener = new ErrorListener('action', null, true, [], $resourceMetadataCollectionFactory->reveal(), ['jsonld' => ['application/ld+json']], [], $identifiersExtractor->reveal(), $resourceClassResolver->reveal(), null, false); + $errorListener->onKernelException($exceptionEvent); + } + + public function testDuplicateExceptionWithErrorResourceProduction(): void + { + $exception = new \Exception(); + $operation = new Get(name: '_api_errors_hydra', priority: 0, outputFormats: ['jsonld' => ['application/ld+json']]); + $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactory->create(Error::class) + ->willReturn(new ResourceMetadataCollection(Error::class, [new ApiResource(operations: [$operation])])); + $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolver->isResourceClass(\Exception::class)->willReturn(false); + $kernel = $this->prophesize(KernelInterface::class); + $kernel->handle(Argument::that(function ($request) { + $this->assertTrue($request->attributes->has('_api_original_route')); + $this->assertTrue($request->attributes->has('_api_original_route_params')); + $this->assertTrue($request->attributes->has('_api_requested_operation')); + $this->assertTrue($request->attributes->has('_api_previous_operation')); + $this->assertEquals('_api_errors_hydra', $request->attributes->get('_api_operation_name')); + $operation = $request->attributes->get('_api_operation'); + $this->assertEquals(\call_user_func($operation->getProvider())->getDetail(), 'Internal Server Error'); + + return true; + }), HttpKernelInterface::SUB_REQUEST, false)->willReturn(new Response()); + $exceptionEvent = new ExceptionEvent($kernel->reveal(), Request::create('/'), HttpKernelInterface::SUB_REQUEST, $exception); + $identifiersExtractor = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractor->getIdentifiersFromItem(Argument::cetera())->willThrow(new \Exception()); + $errorListener = new ErrorListener('action', null, false, [], $resourceMetadataCollectionFactory->reveal(), ['jsonld' => ['application/ld+json']], [], $identifiersExtractor->reveal(), $resourceClassResolver->reveal()); + $errorListener->onKernelException($exceptionEvent); + } }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-rfw5-cqjj-7v9rghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-47639ghsaADVISORY
- github.com/api-platform/core/commit/ba8a7e6538bccebf14c228e43a9339214c4d9201nvdWEB
- github.com/api-platform/core/pull/5823nvdWEB
- github.com/api-platform/core/security/advisories/GHSA-rfw5-cqjj-7v9rnvdWEB
News mentions
0No linked articles in our index yet.