myexploit.py created to reproduce CVE-2026-22738.August 9, 2026
Spring AI — SpEL Injection Leading to Remote Code Execution (CVE-2026–22738)
AI is supposed to make our lives easier. You ask it a question. It finds the information. It gives you an answer. Everyone is happy. But…

By Raj Kumar M
8 min read
AI is supposed to make our lives easier. You ask it a question. It finds the information. It gives you an answer. Everyone is happy. But there is one small problem.
What happens when the AI application's storage decides to trust the wrong thing?
In May 2025, Spring AI was officially released as the first stable version of a Java framework that helps developers add AI features to their applications. Instead of building everything from scratch, developers can use Spring AI to connect their Spring Boot applications with AI services such as OpenAI, Ollama, and other LLM providers. This made it much easier to build things like AI chatbots, document search, AI assistants, and other AI-powered features. As more companies started using AI, Spring AI also began appearing in real-world applications.
But then, things got interesting.
On 26 March 2026, a critical vulnerability called CVE-2026–22738 was disclosed in Spring AI. The vulnerability affects a component called SimpleVectorStore. In simple terms, this component is used to store and retrieve information that an AI application needs. The vulnerability has a CVSS score of 9.8 out of 10, which is considered Critical.
So, why is this such a big deal?
An attacker may not need a username, password, or even a victim to click a link. If the vulnerable functionality is exposed to the internet, an attacker could send a specially crafted HTTP request to the application and potentially execute commands on the server.
Think about it like this:
Find the vulnerable application → Send a malicious request → Execute code on the server. If the attack is successful, the attacker could potentially access sensitive information, modify files, steal credentials, or even take control of the server, depending on the application's permissions and environment. That's what makes this vulnerability worth paying attention to. In this blog, we'll keep things simple and walk through what CVE-2026–22738 is, how SimpleVectorStore is involved, how the vulnerability works, and how developers can protect their applications.
What Does the Vulnerable Request Look Like?
In our test application, the search functionality is exposed through:
GET /search?filterKey=country&filterValue=US&query=helloGET /search?filterKey=country&filterValue=US&query=helloAt first glance, this looks like a normal search request.
The parameters are straightforward:
filterKey— the metadata field used for filteringfilterValue— the value the application should matchquery— the actual search term
So, the request basically means:
Search for
hello, but only return documents where thecountrymetadata isUS.
The interesting part is what happens to filterKey after the request reaches the application.
A simplified flow looks like this:
Attacker
↓
/search
↓
filterKey
↓
SearchRequest
↓
SimpleVectorStore
↓
SpEL EvaluationAttacker
↓
/search
↓
filterKey
↓
SearchRequest
↓
SimpleVectorStore
↓
SpEL EvaluationIf the application encounters an exception while processing the supplied expression, it may return:
HTTP/1.1 500 Internal Server ErrorHTTP/1.1 500 Internal Server ErrorA 500 response simply means that the server encountered an error while processing the request. It is not, by itself, proof of CVE-2026-22738.
The important thing is whether the supplied input actually reaches the vulnerable expression-evaluation logic.
There is also no fixed endpoint such as /api/cve-2026-22738. Spring AI does not automatically expose a specific URL for this vulnerability. The vulnerable endpoint depends on how the application developer exposes the SimpleVectorStore functionality.
For example, an application could expose the search functionality through:
/api/search
/api/documents/search
/api/vector-search/api/search
/api/documents/search
/api/vector-searchThese are only examples. In our test application, the endpoint is /search.
How Does CVE-2026–22738 Actually Work?
Now that we know what the request looks like, let's look at what happens behind the scenes.
When the application receives:
GET /search?filterKey=country&filterValue=US&query=helloGET /search?filterKey=country&filterValue=US&query=helloit creates a SearchRequest and passes the search operation to SimpleVectorStore.
The simplified flow is:
GET /search
↓
filterKey=country
filterValue=US
query=hello
↓
SearchRequest
↓
SimpleVectorStore
↓
FilterExpressionEvaluator
↓
SpEL
↓
Search ResultsGET /search
↓
filterKey=country
filterValue=US
query=hello
↓
SearchRequest
↓
SimpleVectorStore
↓
FilterExpressionEvaluator
↓
SpEL
↓
Search ResultsThe normal use case is straightforward. The application wants to turn the filter information into an expression that checks the document metadata.
For example:
#metadata['country'] == 'US'#metadata['country'] == 'US'This means:
Check whether the document's
countrymetadata is equal toUS.
The problem starts when attacker-controlled input is allowed to influence the expression that is eventually evaluated by SpEL.
Where the Problem Starts
To make the issue easier to understand, the vulnerable logic can be represented in simplified form as:
String spel =
"#metadata['" + filterKey + "'] == '" + filterValue + "'";String spel =
"#metadata['" + filterKey + "'] == '" + filterValue + "'";With normal input:
filterKey = country
filterValue = USfilterKey = country
filterValue = USthe resulting expression becomes:
#metadata['country'] == 'US'#metadata['country'] == 'US'That is the intended behavior.
The security problem is that part of the expression is influenced by user-controlled input.
The resulting expression is then handled by:
SimpleVectorStoreFilterExpressionEvaluatorSimpleVectorStoreFilterExpressionEvaluatorThis is where the evaluation context becomes important.
StandardEvaluationContext vs SimpleEvaluationContext
SpEL uses an evaluation context to determine what an expression is allowed to access and execute.
A simple way to think about it is:
The evaluation context defines how much power the expression has.
There are two contexts that are important here:
ContextWhat it allowsSuitable forStandardEvaluationContextPowerful operations such as method calls and access to Java typesTrusted inputSimpleEvaluationContextRestricted property and data accessUntrusted input
StandardEvaluationContext provides considerably more functionality.
One of the SpEL features available in this context is the T(...) operator.
For example:
T(java.lang.System)T(java.lang.System)allows an expression to refer to the Java System class.
Similarly:
T(java.lang.Runtime)T(java.lang.Runtime)refers to the Java Runtime class.
This becomes a security concern when an attacker can influence an expression that is evaluated using a powerful context.
The difference can be summarized simply:
Trusted input
↓
Powerful expression evaluator
↓
Lower risk
Untrusted input
↓
Powerful expression evaluator
↓
Potential security issueTrusted input
↓
Powerful expression evaluator
↓
Lower risk
Untrusted input
↓
Powerful expression evaluator
↓
Potential security issueThat combination is the key to understanding this vulnerability.
Why Is T(...) Dangerous?
The T(...) operator is not a vulnerability by itself.
It is simply a SpEL feature that allows an expression to refer to a Java type.
For example:
T(java.lang.System)T(java.lang.System)refers to the Java System class.
Likewise:
T(java.lang.Runtime)T(java.lang.Runtime)refers to the Java Runtime class.
The security problem appears when an attacker can influence an expression that has access to these powerful Java features.
The Runtime class is particularly important because it provides functionality that can interact with the underlying operating system.
This is where the issue can move from:
Input ManipulationInput Manipulationto:
Expression EvaluationExpression Evaluationand potentially to:
Remote Code ExecutionRemote Code ExecutionIn simple terms:
Attacker-controlled input
↓
SpEL expression
↓
Powerful evaluation context
↓
Access to Java functionality
↓
Potential OS-level executionAttacker-controlled input
↓
SpEL expression
↓
Powerful evaluation context
↓
Access to Java functionality
↓
Potential OS-level executionSo the real problem is not T(...) alone.
It is the combination of untrusted input + powerful SpEL evaluation.
Understanding the Attack Surface
The next question is: when is an application actually at risk?
The application becomes interesting from a security perspective when it accepts user-controlled filter information and passes that information into the SearchRequest used by SimpleVectorStore.
The important data flow is:
HTTP Request
↓
User-controlled filter input
↓
SearchRequest
↓
SimpleVectorStore
↓
Filter Expression
↓
SpEL EvaluationHTTP Request
↓
User-controlled filter input
↓
SearchRequest
↓
SimpleVectorStore
↓
Filter Expression
↓
SpEL EvaluationFor our test application:
GET /search?filterKey=country&filterValue=US&query=helloGET /search?filterKey=country&filterValue=US&query=helloThe important parameter is filterKey.
The endpoint name itself is not what makes the application vulnerable. Another application could expose exactly the same functionality through a completely different URL.
What matters is the data flow from the HTTP request to the expression evaluator.
Why filterKey Is Important
The reason filterKey matters is not simply because it controls the metadata field.
The security issue appears when attacker-controlled input is allowed to become part of an expression that is later evaluated by SpEL.
Under normal conditions, the application expects something like:
filterKey = country
filterValue = USfilterKey = country
filterValue = USand creates a filter such as:
#metadata['country'] == 'US'#metadata['country'] == 'US'That is normal application behavior.
The problem occurs when the application does not treat the value only as data and instead allows it to influence the expression itself.
The difference is important:
Normal:
User input → Data
Vulnerable:
User input → Expression → JVM evaluationNormal:
User input → Data
Vulnerable:
User input → Expression → JVM evaluationOnce attacker-controlled input reaches a powerful expression evaluator, the attacker may be able to access functionality that was never intended to be exposed through a normal search parameter.
That is what creates the path toward SpEL injection and potentially Remote Code Execution.
A Similar Problem: CVE-2022–22963
This type of issue is not completely new in the Spring ecosystem.
In 2022, CVE-2022–22963 affected Spring Cloud Function and involved attacker-controlled input being evaluated as a SpEL expression using a powerful evaluation context.
The important lesson is the same:
Expression languages are powerful, but they become dangerous when untrusted input is allowed to control what gets evaluated.
Developers should therefore be especially careful when expression languages are used with values coming from HTTP parameters, headers, API requests, or other user-controlled sources.
It is also important not to confuse this issue with Spring4Shell (CVE-2022–22965). Spring4Shell is a different vulnerability and does not follow this same SpEL-injection pattern.
Mapping the Attack to MITRE ATT&CK
From an attacker's perspective, exploitation of an exposed vulnerable application can be mapped to:
T1190 — Exploit Public-Facing Application
The attacker first identifies an application exposed to the network and then attempts to exploit the vulnerable functionality.
In our scenario:
Internet
↓
Exposed Spring AI Application
↓
Search Endpoint
↓
CVE-2026-22738Internet
↓
Exposed Spring AI Application
↓
Search Endpoint
↓
CVE-2026-22738If successful exploitation results in command execution on a Linux system, the activity may also relate to:
T1059.004 — Unix Shell
The overall attack path can therefore be summarized as:
Find exposed application
↓
Identify vulnerable search functionality
↓
Send attacker-controlled input
↓
Input reaches SpEL evaluation
↓
Potential code execution
↓
Potential server compromiseFind exposed application
↓
Identify vulnerable search functionality
↓
Send attacker-controlled input
↓
Input reaches SpEL evaluation
↓
Potential code execution
↓
Potential server compromiseThe important takeaway is that the vulnerability is not simply the existence of a /search endpoint.
The real problem is that untrusted input from the application can reach a powerful expression evaluator inside the JVM.
Reproducing the Vulnerability
After understanding the vulnerable code path, I wanted to verify whether the issue could actually be exploited in the lab environment.
Instead of using a pre-built exploit, I created my own Python script, myexploit.py, to send the crafted request to the vulnerable /search endpoint.
I also tested the same request directly using curl.
This gave me two ways to reproduce the vulnerability:
myexploit.py
↓
Crafted HTTP Request
↓
/search
↓
SpEL Evaluation
↓
Remote Code Executionmyexploit.py
↓
Crafted HTTP Request
↓
/search
↓
SpEL Evaluation
↓
Remote Code Executionand:
curl
↓
Crafted HTTP Request
↓
/search
↓
SpEL Evaluation
↓
Remote Code Executioncurl
↓
Crafted HTTP Request
↓
/search
↓
SpEL Evaluation
↓
Remote Code ExecutionBoth approaches reached the same vulnerable code path.
Method 1: Using myexploit.py
I first created my own Python script, myexploit.py, to generate and send the crafted request.
The script sends the malicious value through the filterKey parameter of the /search endpoint.
The important part here is that the payload is passed through the same parameter that we identified earlier as reaching the SpEL expression.
After running the script, the vulnerable application processed the supplied expression and the controlled test resulted in operating-system-level command execution.
This confirmed that the vulnerability was not limited to generating an error response. The attacker-controlled input could reach the underlying command-execution functionality.
Method 2: Direct curl Request
I then reproduced the same vulnerability without using Python.
For this test, I sent the crafted request directly to the /search endpoint using curl.
The request contains the malicious expression inside the URL-encoded filterKey parameter.
For readability, the payload is not expanded here because the important part is understanding the request flow rather than copying the complete command.
The command returned:
500500This corresponds to:
HTTP/1.1 500 Internal Server ErrorHTTP/1.1 500 Internal Server ErrorAt first, the 500 response may look like the request simply failed. However, it was a useful indicator that the application had processed the supplied input.
It is important to remember that the 500 response alone does not prove Remote Code Execution.
The actual confirmation came from the controlled execution result obtained in the lab environment.
The important point is that the 500 response was not the final proof of RCE.
The actual confirmation came from the controlled execution result obtained in the lab.