# Cantina case study: SpEL injection in Spring AI explained > Spring AI's SimpleVectorStore is vulnerable to remote code execution through SpEL injection in filter expressions when keys are not properly sanitized. Author: Cantina Published: April 6, 2026 Topics: Security, Spring AI, SpEL Injection, CVE-2026-22738, Vector Stores, RCE, AppSec Canonical URL: https://www.cantina.security/blog/spring-ai-spel-injection-cve-2026-22738 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: ![code-1](./spring-ai-spel-injection-cve-2026-22738/spring-ai-spel-injection-cve-2026-22738-image-1.png) 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: ![simplevectorstore](./spring-ai-spel-injection-cve-2026-22738/spring-ai-spel-injection-cve-2026-22738-image-2.png) 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: ![simplevectorstorefilter](./spring-ai-spel-injection-cve-2026-22738/spring-ai-spel-injection-cve-2026-22738-image-3.png) But doKey() never calls emitSpelString(). It calls removeOuterQuotes(), a method that strips the first and last character and returns everything between them, unexamined: ![abstractfilter](./spring-ai-spel-injection-cve-2026-22738/spring-ai-spel-injection-cve-2026-22738-image-4.png) 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: ![filtersg4](./spring-ai-spel-injection-cve-2026-22738/spring-ai-spel-injection-cve-2026-22738-image-5.png) 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: ![nullort](./spring-ai-spel-injection-cve-2026-22738/spring-ai-spel-injection-cve-2026-22738-image-6.png) 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: ![metadatanull](./spring-ai-spel-injection-cve-2026-22738/spring-ai-spel-injection-cve-2026-22738-image-7.png) 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: ![simplevectorstore](./spring-ai-spel-injection-cve-2026-22738/spring-ai-spel-injection-cve-2026-22738-image-8.png) 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: ![importorg](./spring-ai-spel-injection-cve-2026-22738/spring-ai-spel-injection-cve-2026-22738-image-9.png) The new evaluator resolves keys by direct Map.get() on the metadata, with no intermediate string representation: ![simplevectorstore](./spring-ai-spel-injection-cve-2026-22738/spring-ai-spel-injection-cve-2026-22738-image-10.png) 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-store` versions `>= 1.0.0, < 1.0.5` and `>= 1.1.0-M1, < 1.1.4` * Fixed in **1.0.5** and **1.1.4** * Only applications using `SimpleVectorStore` with 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()` versus `doKey()` in the same converter class. * **StandardEvaluationContext is almost always wrong for user-influenced expressions.** Spring's own documentation recommends `SimpleEvaluationContext` when evaluating expressions that may contain untrusted content, precisely because `StandardEvaluationContext` enables type references and method invocation. But switching to `SimpleEvaluationContext` would 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](/demo).**