Cantina Case Study: SpEL Injection in Spring AI Explained

Identified by Cantina's AI AppSec tool.
Any Spring AI application using SimpleVectorStore with user-supplied filter expressions is vulnerable to remote code execution. The root cause: the filter-to-SpEL converter sanitizes values but not keys, so a quoted identifier in a filter expression can break out of a metadata accessor and inject arbitrary Spring Expression Language. CVE-2026-22738, CVSS 9.8.
Apex identified the vulnerability by tracing how user-controlled filter expressions flow from the RAG query pipeline through the ANTLR parser and into SpEL evaluation with an unrestricted StandardEvaluationContext.
The fix, shipped in Spring AI 1.0.5 and 1.1.4, eliminates SpEL entirely. Filter expressions are now evaluated by walking the AST directly instead of converting to an intermediate string representation.
How SimpleVectorStore Evaluates Filters
Spring AI's RAG pipeline lets users pass metadata filter expressions to narrow vector similarity searches. A chatbot might filter documents by year, author, or department, which are metadata fields attached to each embedded document.
When a filter expression reaches SimpleVectorStore, it goes through three stages: parse, convert, evaluate. The ANTLR-based FilterExpressionTextParser parses the filter string into an AST. A FilterExpressionConverter then converts that AST into a SpEL string. Finally, SimpleVectorStore evaluates the SpEL against each document's metadata.
The entry point from the RAG retriever:

The FILTER_EXPRESSION context key ("vector_store_filter_expression") accepts a raw string that gets parsed directly. This string can originate from user input: a chat message, an API parameter, or an LLM-generated query augmentation.
The Injection: Keys Are Not Values
The vulnerability lives in SimpleVectorStoreFilterExpressionConverter.doKey(). This method takes a filter key (a metadata field name) and interpolates it into a SpEL metadata accessor:

The converter strips outer quotes from the key and splices the inner content directly into #metadata['...']. No escaping. No validation.
For values, the converter does the right thing: emitSpelString() escapes single quotes and backslashes:

But doKey() never calls emitSpelString(). It calls removeOuterQuotes(), a method that strips the first and last character and returns everything between them, unexamined:

The asymmetry is the bug. Values are treated as untrusted data. Keys are treated as safe identifiers. But the ANTLR grammar allows keys to be QUOTED_STRING, arbitrary content between matching quotes:

A double-quoted string can contain any characters except unescaped " or \\. Single quotes, SpEL syntax, type references, all pass through the lexer as a valid identifier.
From Filter Expression to Code Execution
A filter expression like this is syntactically valid:

The ANTLR parser sees a quoted identifier ("..."), the == operator, and a string value ('ignored'). It produces a Filter.Key containing the full double-quoted string.
The converter's doKey() strips the outer "..." and interpolates:

This is valid SpEL. The injected T() type references resolve classes from the classpath, and StandardEvaluationContext, unlike the restricted SimpleEvaluationContext, permits type resolution and method invocation:

The expression is evaluated once per document in the store. Every document triggers the payload. The attacker gets arbitrary file writes, HTTP requests, process execution, or anything reachable from the JVM classpath.
Attack requirements are minimal: the application must use SimpleVectorStore (Spring AI's default in-memory vector store) and pass any user-influenced string as a filter expression. The RAG pipeline's VectorStoreDocumentRetriever does this by default when vector_store_filter_expression appears in the query context.
The Fix
Commit ba9220b eliminates SpEL from the filter evaluation pipeline entirely. The SimpleVectorStoreFilterExpressionConverter, which converted the filter AST to a SpEL string, is deleted and replaced with SimpleVectorStoreFilterExpressionEvaluator, which walks the AST directly:

The new evaluator resolves keys by direct Map.get() on the metadata, with no intermediate string representation:

No SpEL parser. No expression context. No type resolution. The key is used as a literal map lookup, making it injection-proof by construction.
A companion commit (ccc29d10) also fixes the ANTLR parser to properly unquote and unescape quoted identifiers at parse time, so downstream converters for other vector stores receive clean key names rather than raw quoted strings.
Affected Versions
org.springframework.ai:spring-ai-vector-storeversions>= 1.0.0, < 1.0.5and>= 1.1.0-M1, < 1.1.4- Fixed in 1.0.5 and 1.1.4
- Only applications using
SimpleVectorStorewith user-supplied filter expressions are affected - Other vector store implementations (Pinecone, Weaviate, Chroma, etc.) use their own filter converters and are not vulnerable to this specific SpEL injection
Disclosure Timeline
- 2026-03-17: Fix committed to Spring AI repository (ba9220b)
- 2026-03-27: CVE-2026-22738 published; Spring AI 1.0.5 and 1.1.4 released
Takeaways
- The value/key sanitization asymmetry is a class of bug worth looking for. Wherever a system applies escaping or parameterization to one category of user input (values) but not another (keys, column names, identifiers), injection surfaces hide. SQL has this problem with column-name interpolation; Spring AI had it with SpEL metadata accessors. Apex caught this by comparing the sanitization applied to
doValue()versusdoKey()in the same converter class. - StandardEvaluationContext is almost always wrong for user-influenced expressions. Spring's own documentation recommends
SimpleEvaluationContextwhen evaluating expressions that may contain untrusted content, precisely becauseStandardEvaluationContextenables type references and method invocation. But switching toSimpleEvaluationContextwould only have reduced impact; the correct fix was to eliminate string-based expression evaluation entirely. - Eliminating the intermediate representation is stronger than escaping it. The fix doesn't try to sanitize keys before splicing them into SpEL. It removes the SpEL layer completely. Walking the AST directly makes injection structurally impossible because there is no string to break out of. When a filter system converts a structured AST to a string only to parse it back into executable form, that round-trip is the vulnerability.
Secure Your Codebase Autonomously
AI frameworks evolve rapidly, and complex vulnerabilities require deep contextual understanding to catch. Apex acts as an AI-powered safety net that understands your codebase, identifies risks without the noise of false positives, and generates the Pull Requests needed to fix them.
You ship. Apex handles the rest.
Try Apex for yourself, book a demo here.