CVE-2025-31481: GraphQL Security Bypass in API Platform Core via Relay Node Identification
Executive Summary
CVE-2025-31481 is a critical vulnerability affecting API Platform Core versions prior to 4.0.22. This vulnerability allows attackers to bypass configured security measures on GraphQL query operations by leveraging the Relay special node type. The root cause lies in the improper handling of Relay node identification, leading to incorrect authorization checks. Successful exploitation can result in unauthorized access to sensitive data. The vulnerability has been assigned a CVSS score of 7.5, indicating a high severity.
Technical Details
The vulnerability resides within the API Platform Core, a framework designed for building hypermedia-driven REST and GraphQL APIs. Specifically, the issue arises when using the Relay specification for GraphQL. Relay introduces the concept of "nodes," which are objects identifiable by a global ID. This ID is intended to provide a consistent way to fetch objects regardless of their underlying type.
Affected Systems:
- Systems using API Platform Core to expose GraphQL APIs.
Affected Software:
- API Platform Core versions prior to 4.0.22.
Affected Components:
src/GraphQl/Metadata/RuntimeOperationMetadataFactory.php
src/GraphQl/Resolver/Factory/ResolverFactory.php
The vulnerability stems from how API Platform Core handles Relay node identification and authorization within its GraphQL resolver. The ResolverFactory
is responsible for creating resolvers that fetch and process data based on GraphQL queries. When a Relay node is requested, the ResolverFactory
needs to determine the appropriate operation to execute. Prior to the patch, the operation was defaulted to a Query
without proper authorization checks.
Root Cause Analysis
The core issue is the lack of proper operation identification and authorization when resolving Relay nodes. The ResolverFactory
was not correctly identifying the specific GraphQL operation associated with a Relay node ID, leading to a bypass of configured security rules.
Before the patch, the resolve
method in ResolverFactory.php
contained the following logic:
private function resolve(?array $source, array $args, ResolveInfo $info, ?string $rootClass = null, ?Operation $operation = null, mixed $body = null)
{
// Handles relay nodes
$operation ??= new Query();
$graphQlContext = [];
$context = ['source' => $source, 'args' => $args, 'info' => $info, 'root_class' => $rootClass, 'graphql_context' => &$graphQlContext];
// ...
}
This code snippet shows that if no operation was explicitly provided (which is the case when resolving a Relay node by its ID), a new Query
operation was created. This new Query
operation bypassed any security configurations defined for specific GraphQL operations, such as access control rules or authorization checks. An attacker could craft a GraphQL query using the node
field and a valid Relay ID to access resources they were not authorized to view.
The RuntimeOperationMetadataFactory
class is designed to determine the correct operation based on the Relay node ID. However, it was not being used in the ResolverFactory
before the patch.
Patch Analysis
The patch addresses the vulnerability by ensuring that the correct GraphQL operation is identified and authorized when resolving Relay nodes. The key changes are in src/GraphQl/Resolver/Factory/ResolverFactory.php
and the introduction of src/GraphQl/Metadata/RuntimeOperationMetadataFactory.php
.
1. Dependency Injection of OperationMetadataFactoryInterface
:
The ResolverFactory
now requires an OperationMetadataFactoryInterface
instance to be injected during construction. This ensures that the factory can retrieve operation metadata based on the Relay node ID.
--- a/src/GraphQl/Resolver/Factory/ResolverFactory.php
+++ b/src/GraphQl/Resolver/Factory/ResolverFactory.php
@@ -15,21 +15,28 @@
use ApiPlatform\GraphQl\State\Provider\NoopProvider;
use ApiPlatform\Metadata\DeleteOperationInterface;
+use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\GraphQl\Query;
+use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\State\Pagination\ArrayPaginator;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface;
use GraphQL\Type\Definition\ResolveInfo;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ResolverFactory implements ResolverFactoryInterface
{
public function __construct(
private readonly ProviderInterface $provider,
private readonly ProcessorInterface $processor,
+ private readonly ?OperationMetadataFactoryInterface $operationMetadataFactory = null,
) {
+ if (!$operationMetadataFactory) {
+ throw new InvalidArgumentException(\sprintf('Not injecting the "%s" exposes Relay nodes to a security risk.', OperationMetadataFactoryInterface::class));
+ }
}
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
This change ensures that the OperationMetadataFactoryInterface
is injected and throws an exception if it is not, highlighting the security risk.
2. Operation Resolution using OperationMetadataFactoryInterface
:
The resolve
method in ResolverFactory.php
now uses the injected OperationMetadataFactoryInterface
to retrieve the correct operation based on the Relay node ID.
--- a/src/GraphQl/Resolver/Factory/ResolverFactory.php
+++ b/src/GraphQl/Resolver/Factory/ResolverFactory.php
@@ -70,7 +77,13 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul
private function resolve(?array $source, array $args, ResolveInfo $info, ?string $rootClass = null, ?Operation $operation = null, mixed $body = null)
{
// Handles relay nodes
- $operation ??= new Query();
+ if (!$operation) {
+ if (!isset($args['id'])) {
+ throw new NotFoundHttpException('No node found.');
+ }
+
+ $operation = $this->operationMetadataFactory->create($args['id']);
+ }
$graphQlContext = [];
$context = ['source' => $source, 'args' => $args, 'info' => $info, 'root_class' => $rootClass, 'graphql_context' => &$graphQlContext];
This code now checks if an operation is already provided. If not (indicating a Relay node request), it retrieves the operation using $this->operationMetadataFactory->create($args['id'])
. This ensures that the correct operation, with its associated security configurations, is used to resolve the Relay node. If the 'id' argument is missing, it throws a NotFoundHttpException
.
3. Implementation of RuntimeOperationMetadataFactory
:
The RuntimeOperationMetadataFactory
class implements the OperationMetadataFactoryInterface
. It is responsible for retrieving the operation metadata based on the Relay node ID. It uses the ResourceMetadataCollectionFactoryInterface
and RouterInterface
to determine the resource class and operation name associated with the ID.
<?php
declare(strict_types=1);
namespace ApiPlatform\GraphQl\Metadata;
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface;
use Symfony\Component\Routing\RouterInterface;
/**
* This factory runs in the ResolverFactory and is used to find out a Relay node's operation.
*/
final class RuntimeOperationMetadataFactory implements OperationMetadataFactoryInterface
{
public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly RouterInterface $router)
{
}
public function create(string $uriTemplate, array $context = []): ?Operation
{
try {
$parameters = $this->router->match($uriTemplate);
} catch (RoutingExceptionInterface $e) {
throw new InvalidArgumentException(\sprintf('No route matches "%s".', $uriTemplate), $e->getCode(), $e);
}
if (!isset($parameters['_api_resource_class'])) {
throw new InvalidArgumentException(\sprintf('The route "%s" is not an API route, it has no resource class in the defaults.', $uriTemplate));
}
foreach ($this->resourceMetadataCollectionFactory->create($parameters['_api_resource_class']) as $resource) {
foreach ($resource->getGraphQlOperations() ?? [] as $operation) {
if ($operation instanceof Query && !$operation->getResolver()) {
return $operation;
}
}
}
throw new InvalidArgumentException(\sprintf('No operation found for id "%s".', $uriTemplate));
}
}
This class first uses the RouterInterface
to match the URI template (Relay node ID) to a route. It then extracts the resource class from the route parameters. Finally, it iterates through the GraphQL operations defined for the resource and returns the first Query
operation that does not have a resolver defined. This ensures that the correct operation is used for resolving the Relay node.
4. Configuration Changes:
The graphql.xml
configuration file is updated to register the RuntimeOperationMetadataFactory
as a service and inject the necessary dependencies.
--- a/src/Symfony/Bundle/Resources/config/graphql.xml
+++ b/src/Symfony/Bundle/Resources/config/graphql.xml
@@ -187,6 +187,12 @@
<service id="api_platform.graphql.resolver.factory" class="ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory" public="false">
<argument type="service" id="api_platform.graphql.state_provider" />
<argument type="service" id="api_platform.graphql.state_processor" />
+ <argument type="service" id="api_platform.graphql.runtime_operation_metadata_factory" />
+ </service>
+
+ <service id="api_platform.graphql.runtime_operation_metadata_factory" class="ApiPlatform\GraphQl\Metadata\RuntimeOperationMetadataFactory" public="false">
+ <argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
+ <argument type="service" id="api_platform.router" />
</service>
<!-- Resolver Stages -->
This ensures that the ResolverFactory
has access to the RuntimeOperationMetadataFactory
and can correctly resolve Relay nodes.
Exploitation Techniques
An attacker can exploit this vulnerability by crafting a GraphQL query that uses the node
field to request a Relay node. By providing a valid Relay ID, the attacker can bypass security checks and access resources they are not authorized to view.
Proof-of-Concept (PoC) Example:
Assume there is a SecuredDummy
entity with a GraphQL query operation that requires authentication. Without the patch, an attacker could bypass this authentication requirement by using the node
field.
First, the attacker needs to obtain a valid Relay ID for a SecuredDummy
entity. This ID typically follows the format /secured_dummies/{id}
.
Then, the attacker can craft the following GraphQL query:
{
node(id: "/secured_dummies/1") {
... on SecuredDummy {
title
description
}
}
}
Before the patch, this query would bypass the authentication check and return the title
and description
of the SecuredDummy
entity, even if the attacker was not authenticated.
Attack Scenario:
- An attacker identifies a GraphQL endpoint exposed by an API Platform Core application.
- The attacker discovers that the application uses Relay for node identification.
- The attacker obtains a valid Relay ID for a resource they are not authorized to access.
- The attacker crafts a GraphQL query using the
node
field and the Relay ID. - The attacker sends the query to the GraphQL endpoint.
- The application, due to the vulnerability, bypasses security checks and returns the requested resource data.
Real-World Impacts:
- Data Breach: Unauthorized access to sensitive data, such as user profiles, financial information, or confidential documents.
- Privilege Escalation: An attacker may be able to access resources or perform actions that are normally restricted to users with higher privileges.
- Reputation Damage: A successful attack can damage the reputation of the organization using the vulnerable API Platform Core application.
Mitigation Strategies
To mitigate CVE-2025-31481, the following strategies are recommended:
- Upgrade to API Platform Core 4.0.22 or later: This is the primary and most effective mitigation. Upgrading to the patched version eliminates the vulnerability.
- Implement Custom Authorization Logic: Even with the patch, it's crucial to implement robust authorization logic within your GraphQL resolvers. This can involve checking user roles, permissions, and other contextual factors to ensure that only authorized users can access specific resources.
- Regular Security Audits: Conduct regular security audits of your API Platform Core applications to identify and address potential vulnerabilities.
- Web Application Firewall (WAF): Deploy a WAF to detect and block malicious GraphQL queries, such as those attempting to exploit this vulnerability. Configure the WAF with rules to identify and block suspicious patterns in GraphQL requests.
- Input Validation: Implement strict input validation to prevent attackers from injecting malicious data into GraphQL queries. This can involve validating the format, type, and range of input values.
Timeline of Discovery and Disclosure
- 2025-03-28: Vulnerability reported.
- 2025-04-03: Patch released in API Platform Core 4.0.22.
- 2025-04-03: Public disclosure of CVE-2025-31481.