Feb 20, 2026·6 min read·92 visits
API Platform's GraphQL implementation dropped the ball on the Relay `node` query. By asking for a resource via its global ID (IRI) through the `node` field, the system failed to load the specific security rules attached to that resource. This means an unauthenticated attacker could access data restricted to admins simply by knowing the resource's ID URL.
A critical authentication bypass vulnerability in API Platform Core's GraphQL subsystem allows attackers to read protected resources by leveraging the Relay `node` interface. The flaw stems from a failure to correctly load security operations when resolving Global Object Identifiers (IRIs), effectively neutralizing access control lists (ACLs) for specific queries.
GraphQL is magical. It lets front-end developers ask for exactly what they want, and the backend just... provides. One of the most powerful features in the GraphQL ecosystem is the Relay Global Object Identification specification. It introduces a standardized interface called Node. The promise is simple: give me a global ID (usually an IRI like /users/1), and I will return the object, no matter what type it is.
It’s the ultimate skeleton key for data fetching. You don't need to know if you are querying a User, a Product, or a SecretDocument; you just ask for the Node, and the server resolves the type for you. But here is the problem with magic skeleton keys: if you forget to check who is holding them, you are going to have a bad time.
In API Platform, a framework designed to automagically build APIs around your PHP classes, this convenience feature had a nasty side effect. While the framework is usually paranoid about checking @Security annotations and voters before handing out data, the node query path found a loophole. It was the digital equivalent of a bouncer checking IDs at the VIP entrance, while leaving the loading dock wide open because 'surely nobody knows where the loading dock is.' (Spoiler: We always know where the loading dock is).
To understand this bug, you have to understand how API Platform handles requests. When you hit a standard endpoint, say query { book(id: "/books/1") { ... } }, the system knows exactly what you are doing. It maps the request to a specific Operation on the Book resource. That Operation carries metadata, including security rules like is_granted('ROLE_USER').
However, the Relay node query is polymorphic. It starts at the root. The request is just query { node(id: "...") }. At the moment the query starts, the system doesn't necessarily know what specific resource operation is being performed. It just knows it needs to resolve an ID.
The vulnerability lived in the ResolverFactory. When processing the node field, the system initialized a context for the resolver. In the affected versions, if the operation context wasn't explicitly passed (which is the case for the root node field), the code defaulted to a generic, empty Query object.
Here is the kicker: that generic Query object didn't have your custom security attributes. It was a blank slate. So when the security listener asked, "Are there any rules preventing this user from seeing this?", the generic object replied, "Nope, looks clear to me." The actual resource class (e.g., SecuredDummy) had rules defined, but because the resolver was looking at the generic placeholder operation instead of the specific resource operation, those rules were completely ignored.
Let's look at the smoking gun. The vulnerability was centered in src/GraphQl/Resolver/Factory/ResolverFactory.php. The logic tried to determine which operation was currently being executed to enforce permissions.
In the vulnerable version, the code looked something like this (simplified for clarity):
// Vulnerable Code
$operation = $context['operation'] ?? null;
// If we don't know the operation, just make up a blank one!
$operation ??= new Query();
// ... later ...
if (!$this->security->isGranted($operation->getSecurity(), ...)) {
throw new AccessDeniedException();
}See that ??= new Query()? That is the developer shrugging. Since a fresh new Query() has no security attributes, isGranted checks nothing. It defaults to allowing access because there are no restrictions to enforce.
The fix, applied in commit 60747cc8c2fb855798c923b5537888f8d0969568, forces the system to actually do the homework. Instead of guessing, it now uses a RuntimeOperationMetadataFactory to look up the real operation associated with the IRI.
// Patched Code
try {
// Actually figure out what resource this IRI belongs to
$operation = $this->runtimeOperationMetadataFactory->create($operationName, $resourceClass, true);
} catch (OperationNotFoundException) {
// If we can't find the operation security rules, don't just let them in.
if (null === $operation) {
// Fallback logic that implies strictness or failure
}
}By ensuring the $operation object is hydrated with the metadata from the actual target resource, the security voters are correctly invoked.
Exploiting this is trivially easy if you know the IRI format of the target application. Let's assume we have a resource PrivateDocument that requires ROLE_ADMIN. Normal users should get a 403 Forbidden.
Step 1: The Sanity Check (Failed Access) A standard GraphQL query against the resource works as expected:
query {
privateDocument(id: "/private_documents/1") {
secretContent
}
}Result: "Access Denied." (Because the privateDocument query operation has the security check attached).
Step 2: The Bypass
Now, we switch to the Relay node interface. We ask for the exact same object, but we route the request through the global resolver:
query {
node(id: "/private_documents/1") {
... on PrivateDocument {
id
secretContent
}
}
}Step 3: Execution
Because the node resolver uses the generic Query object (which lacks the ROLE_ADMIN requirement), the API fetches the object from the database and serializes it. The security listener sees an operation with no restrictions and waves it through.
Result:
{
"data": {
"node": {
"id": "/private_documents/1",
"secretContent": "The launch codes are 12345"
}
}
}Here is a visualization of the logic flow:
The impact here is a classic Broken Access Control (CWE-284), but widely distributed. In API Platform, security is often defined centrally on the Resource class (e.g., `#[ApiResource(security:
The maintainers patched this in API Platform Core versions 3.4.17 and 4.0.22. The fix involves a mechanism that properly resolves the underlying resource class from the IRI provided to the node query, ensuring that the correct operation context (and thus the correct security rules) is loaded.
If you are running a vulnerable version, you have two choices:
node query. If your frontend application does not rely on Relay standards, you can disable the node definition in your GraphQL configuration. This removes the attack vector entirely.To developers using API Platform: Do not assume global mechanisms inherit specific constraints. Explicitly test your permissions models using the node query, not just your custom query endpoints.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
API Platform Core api-platform | >= 4.0.0, < 4.0.22 | 4.0.22 |
API Platform Core api-platform | < 3.4.17 | 3.4.17 |
| Attribute | Detail |
|---|---|
| Vulnerability Type | Authentication Bypass / Broken Access Control |
| CWE ID | CWE-284 |
| CVSS Score | 7.5 (High) |
| Attack Vector | Network |
| Affected Component | GraphQL ResolverFactory (Relay Node) |
| Exploit Status | PoC Available |
Improper Access Control
CVE-2026-115895 is a high-severity code injection vulnerability (CWE-94) in the happy-dom npm package prior to version 21.0.4. The vulnerability exists within the ECMAScriptModuleCompiler component, which transforms ECMAScript module syntax into executable Node.js functions when JavaScript evaluation is enabled. Due to an incomplete fix for CVE-2026-33943, the compiler failed to validate local named import aliases against legal JavaScript identifier rules. Attackers supplying crafted module imports—specifically utilizing destructuring default initializers alongside JSON module attributes—can inject arbitrary JavaScript expressions directly into the host Node.js runtime context.
An improper input validation and authorization bypass vulnerability exists in the LangGraph storage layer affecting langgraph, langgraph-checkpoint-postgres, and langgraph-checkpoint-sqlite. Because relational store implementations serialize namespace tuples into dot-delimited strings without validating labels on read, search, delete, and batch operations, an attacker who controls a namespace label can inject periods (.) to traverse namespace boundaries and access, modify, or delete data belonging to other namespaces.
CVE-2026-115886 (GHSA-2wqp-q3c6-ccj5) is a Server-Side Request Forgery (SSRF) and Unsafe Reflection vulnerability in MLflow versions 3.12.0rc0 prior to 3.17.0. Unauthenticated REST API endpoints accept serialized third-party scorer JSON payloads that are dynamically deserialized and reflectively instantiated without sanitizing constructor keyword arguments. When optional dependencies such as trulens and trulens-providers-litellm are installed, attackers can inject egress-steering arguments into the scorer payload, forcing the MLflow server to dispatch outbound HTTP requests to arbitrary internal destinations or cloud metadata services.
CVE-2026-113872 (GHSA-q7qp-rjg7-q26m) is a high-severity denial-of-service vulnerability affecting the multer multipart/form-data upload middleware for Node.js and Express in versions 2.0.0 through 2.4.0. When an endpoint is configured to accept multiple files using an asynchronous fileFilter callback, a crafted, truncated multipart HTTP request can trigger an unhandled error event on the underlying busboy stream parser after request teardown has already completed. Because all error listeners are stripped during teardown, Node.js treats the resulting error event as an uncaught exception and immediately terminates the server process.
CVE-2026-115897 (also tracked as GHSA-m5mm-p8h7-m29v and aliased as CVE-2026-115754) is a Cross-Site Scripting (XSS) vulnerability in the Yahoo serialize-javascript npm package prior to version 7.1.3. The flaw exists in the regular expression used by escapeFunctionBody() to neutralize HTML script closing tags within serialized JavaScript functions. Because function bodies are sanitized in isolation before structural formatting is applied, a function whose string representation terminates directly with the sequence '</script' bypasses the positive lookahead check. When combined with serialization formatting options that introduce whitespace delimiters and a subsequent property that supplies a closing bracket, an attacker can prematurely terminate an enclosing HTML <script> block and inject arbitrary HTML or JavaScript into the rendered document.
CVE-2026-109841 is a Subresource Integrity (SRI) verification bypass vulnerability in undici, the official HTTP/1.1 and HTTP/2 client library for Node.js that powers the global fetch() implementation. The flaw stems from improper handling of case sensitivity (CWE-178) when validating cryptographic hash algorithm tokens within SRI metadata. Because undici compared incoming algorithm identifiers against a strictly lowercase set without prior ASCII-lowercase normalization, valid uppercase or mixed-case tokens such as SHA256- or Sha384- were treated as unrecognized algorithms and discarded. Under SRI forward-compatibility rules, discarding all provided tokens results in an empty integrity list, causing undici to fail open and accept tampered HTTP response bodies without cryptographic verification.