Critical severity9.8NVD Advisory· Published Mar 27, 2026· Updated May 10, 2026
CVE-2026-22738
CVE-2026-22738
Description
In Spring AI, a SpEL injection vulnerability exists in SimpleVectorStore when a user-supplied value is used as a filter expression key. A malicious actor could exploit this to execute arbitrary code. Only applications that use SimpleVectorStore and pass user-supplied input as a filter expression key are affected. This issue affects Spring AI: from 1.0.0 before 1.0.5, from 1.1.0 before 1.1.4.
Affected products
1Patches
1ba9220b22383Refactor internal filter evaluation in SimpleVectorStore
8 files changed · +528 −423
spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs.adoc+3 −1 modified@@ -398,7 +398,9 @@ These are the available implementations of the `VectorStore` interface: * xref:api/vectordbs/typesense.adoc[Typesense Vector Store] - The https://typesense.org/docs/0.24.0/api/vector-search.html[Typesense] vector store. * xref:api/vectordbs/weaviate.adoc[Weaviate Vector Store] - The https://weaviate.io/[Weaviate] vector store. * xref:api/vectordbs/s3-vector-store.adoc[S3 Vector Store] - The https://aws.amazon.com/s3/features/vectors/[AWS S3] vector store. -* link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java[SimpleVectorStore] - A simple implementation of persistent vector storage, good for educational purposes. +* link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java[SimpleVectorStore] - A simple implementation of a vector storage, good only for testing purposes. + +WARNING: The `SimpleVectorStore` implementation is not designed for production use and should only be used for testing or demonstration purposes. More implementations may be supported in future releases.
spring-ai-docs/src/main/antora/modules/ROOT/pages/api/vectordbs/understand-vectordbs.adoc+1 −1 modified@@ -91,5 +91,5 @@ It expands the two-dimensional definitions of Magnitude and Dot Product given pr stem:[similarity(vec{A},vec{B}) = \cos(\theta) = \frac{ \sum_{i=1}^{n} {A_i B_i} }{ \sqrt{\sum_{i=1}^{n}{A_i^2} \cdot \sum_{i=1}^{n}{B_i^2}}] **** -This is the key formula used in the simple implementation of a vector store and can be found in the `SimpleVectorStore` implementation. +This is the key formula used in the simple implementation of a vector store and can be found in the `SimpleVectorStore` implementation - applicable for testing and demonstration purposes only.
spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/filter/converter/SimpleVectorStoreFilterExpressionConverter.java+0 −171 removed@@ -1,171 +0,0 @@ -/* - * Copyright 2023-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.ai.vectorstore.filter.converter; - -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.util.Date; -import java.util.List; - -import org.springframework.ai.vectorstore.filter.Filter; -import org.springframework.ai.vectorstore.filter.Filter.Expression; - -/** - * Converts {@link Expression} into SpEL metadata filter expression format. - * (https://docs.spring.io/spring-framework/reference/core/expressions.html) - * - * @author Jemin Huh - */ -public class SimpleVectorStoreFilterExpressionConverter extends AbstractFilterExpressionConverter { - - private final DateTimeFormatter dateFormat; - - public SimpleVectorStoreFilterExpressionConverter() { - this.dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC); - } - - @Override - protected void doExpression(Filter.Expression expression, StringBuilder context) { - this.convertOperand(expression.left(), context); - context.append(getOperationSymbol(expression)); - if (expression.right() != null) { - this.convertOperand(expression.right(), context); - } - else { - context.append("null"); - } - } - - private String getOperationSymbol(Filter.Expression exp) { - return switch (exp.type()) { - case AND -> " and "; - case OR -> " or "; - case EQ -> " == "; - case LT -> " < "; - case LTE -> " <= "; - case GT -> " > "; - case GTE -> " >= "; - case NE -> " != "; - case IN -> " in "; - case NIN -> " not in "; - default -> throw new RuntimeException("Not supported expression type: " + exp.type()); - }; - } - - @Override - protected void doKey(Filter.Key key, StringBuilder context) { - var identifier = hasOuterQuotes(key.key()) ? removeOuterQuotes(key.key()) : key.key(); - context.append("#metadata['").append(identifier).append("']"); - } - - @Override - protected void doValue(Filter.Value filterValue, StringBuilder context) { - if (filterValue.value() instanceof List<?> list) { - var formattedList = new StringBuilder("{"); - int c = 0; - for (Object v : list) { - this.doSingleValue(normalizeDateString(v), formattedList); - if (c++ < list.size() - 1) { - this.doAddValueRangeSpitter(filterValue, formattedList); - } - } - formattedList.append("}"); - - if (context.lastIndexOf("in ") == -1) { - context.append(formattedList); - } - else { - appendSpELContains(formattedList, context); - } - } - else { - this.doSingleValue(normalizeDateString(filterValue.value()), context); - } - } - - private void appendSpELContains(StringBuilder formattedList, StringBuilder context) { - int metadataStart = context.lastIndexOf("#metadata"); - if (metadataStart == -1) { - throw new RuntimeException("Wrong SpEL expression: " + context); - } - int metadataEnd = context.indexOf(" ", metadataStart); - String metadata = context.substring(metadataStart, metadataEnd); - context.setLength(context.lastIndexOf("in ")); - context.delete(metadataStart, metadataEnd + 1); - context.append(formattedList).append(".contains(").append(metadata).append(")"); - } - - /** - * Emit a SpEL-formatted string value with single quote wrapping and escaping by - * appending to the provided context. - * <p> - * Escapes single quotes (using backslash) and backslashes (double backslash) - * according to SpEL string literal rules. - * <p> - * This method prevents SpEL injection attacks by properly escaping special - * characters. - * @param text the string value to format - * @param context the context to append the SpEL string literal to - * @since 2.0.0 - */ - protected static void emitSpelString(String text, StringBuilder context) { - context.append("'"); // Opening quote - - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - - switch (c) { - case '\'': - // SpEL: single quote → backslash escaped - context.append("\\'"); - break; - case '\\': - // SpEL: backslash → double backslash - context.append("\\\\"); - break; - default: - context.append(c); - break; - } - } - - context.append("'"); // Closing quote - } - - @Override - protected void doSingleValue(Object value, StringBuilder context) { - if (value instanceof Date date) { - context.append("'"); - context.append(this.dateFormat.format(date.toInstant())); - context.append("'"); - } - else if (value instanceof String text) { - emitSpelString(text, context); - } - else { - context.append(value); - } - } - - @Override - protected void doGroup(Filter.Group group, StringBuilder context) { - context.append("("); - super.doGroup(group, context); - context.append(")"); - } - -}
spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStoreContent.java+5 −6 modified@@ -38,7 +38,7 @@ * embeddings. This class is thread-safe and all its fields are final and deeply * immutable. The embedding vector is required for all instances of this class. */ -public final class SimpleVectorStoreContent implements Content { +final class SimpleVectorStoreContent implements Content { private final String id; @@ -54,7 +54,7 @@ public final class SimpleVectorStoreContent implements Content { * @param text the content text, must not be null * @param embedding the embedding vector, must not be null */ - public SimpleVectorStoreContent(@JsonProperty("text") @JsonAlias("content") String text, + SimpleVectorStoreContent(@JsonProperty("text") @JsonAlias("content") String text, @JsonProperty("embedding") float[] embedding) { this(text, new HashMap<>(), embedding); } @@ -65,7 +65,7 @@ public SimpleVectorStoreContent(@JsonProperty("text") @JsonAlias("content") Stri * @param metadata the metadata map, must not be null * @param embedding the embedding vector, must not be null */ - public SimpleVectorStoreContent(String text, Map<String, Object> metadata, float[] embedding) { + SimpleVectorStoreContent(String text, Map<String, Object> metadata, float[] embedding) { this(text, metadata, new RandomIdGenerator(), embedding); } @@ -77,8 +77,7 @@ public SimpleVectorStoreContent(String text, Map<String, Object> metadata, float * @param idGenerator the ID generator to use, must not be null * @param embedding the embedding vector, must not be null */ - public SimpleVectorStoreContent(String text, Map<String, Object> metadata, IdGenerator idGenerator, - float[] embedding) { + SimpleVectorStoreContent(String text, Map<String, Object> metadata, IdGenerator idGenerator, float[] embedding) { this(idGenerator.generateId(text, metadata), text, metadata, embedding); } @@ -91,7 +90,7 @@ public SimpleVectorStoreContent(String text, Map<String, Object> metadata, IdGen * @throws IllegalArgumentException if any parameter is null or if id is empty */ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public SimpleVectorStoreContent(@JsonProperty("id") @Nullable String id, + SimpleVectorStoreContent(@JsonProperty("id") @Nullable String id, @JsonProperty("text") @JsonAlias("content") String text, @JsonProperty("metadata") Map<String, Object> metadata, @JsonProperty("embedding") float[] embedding) {
spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStoreFilterExpressionEvaluator.java+221 −0 added@@ -0,0 +1,221 @@ +/* + * Copyright 2023-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.vectorstore; + +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +import org.springframework.ai.vectorstore.filter.Filter; +import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder; + +/** + * Internal helper used by {@link SimpleVectorStore} to evaluate a + * {@link Filter.Expression} AST directly against a document metadata map, without + * converting to an intermediate string representation (e.g. SpEL or SQL). + * + * <p> + * Supports all {@link Filter.ExpressionType} operations: + * <ul> + * <li>Logical: {@code AND}, {@code OR}, {@code NOT}</li> + * <li>Comparison: {@code EQ}, {@code NE}, {@code GT}, {@code GTE}, {@code LT}, + * {@code LTE}</li> + * <li>Collection: {@code IN}, {@code NIN}</li> + * <li>Null checks: {@code ISNULL}, {@code ISNOTNULL}</li> + * </ul> + * + * <p> + * <b>Type handling:</b> Numbers are promoted to {@code double} so that mixed + * {@code Integer}/{@code Long}/{@code Double} metadata values compare correctly. + * {@link Date} filter values are normalised to their ISO-8601 UTC string representation + * (matching the format used to store dates in metadata). + * + * <p> + * <b>Missing-key semantics:</b> A metadata key that is absent is treated as {@code null}. + * Null ordering follows SQL {@code NULLS FIRST} semantics — {@code null} is considered + * less than any non-null value. Consequently, ordered comparisons ({@code GT}, + * {@code GTE}, {@code LT}, {@code LTE}) against a missing key evaluate as if that key + * holds the smallest possible value (e.g. {@code year > 2020} returns {@code false} when + * {@code year} is absent). + * + * @author Christian Tzolov + */ +final class SimpleVectorStoreFilterExpressionEvaluator { + + // 'Z' is intentionally a literal suffix, not the offset pattern 'X'. Combined with + // withZone(UTC) this always produces the fixed form "yyyy-MM-dd'T'HH:mm:ss'Z'", + // matching the format used to store Date values in document metadata. + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") + .withZone(ZoneOffset.UTC); + + /** + * Evaluates the given filter expression against the provided metadata map. + * @param expression the filter expression to evaluate; must not be {@code null} + * @param metadata the document metadata to match against; must not be {@code null} + * @return {@code true} if the metadata satisfies the expression + */ + public boolean evaluate(Filter.Expression expression, Map<String, Object> metadata) { + return evaluateExpression(expression, metadata); + } + + private boolean evaluateOperand(Filter.Operand operand, Map<String, Object> metadata) { + if (operand instanceof Filter.Group group) { + return evaluateOperand(group.content(), metadata); + } + if (operand instanceof Filter.Expression expression) { + return evaluateExpression(expression, metadata); + } + // Filter.Key and Filter.Value are leaf operands consumed directly by + // metadataValue() and filterValue() inside evaluateExpression(). They are never + // passed here as top-level boolean operands, so this branch is unreachable under + // normal usage. + throw new IllegalArgumentException("Unsupported operand type: " + operand.getClass().getName()); + } + + private boolean evaluateExpression(Filter.Expression expression, Map<String, Object> metadata) { + return switch (expression.type()) { + case AND -> evaluateOperand(left(expression), metadata) && evaluateOperand(right(expression), metadata); + case OR -> evaluateOperand(left(expression), metadata) || evaluateOperand(right(expression), metadata); + // Unary operator: only the left operand is used. Ignore right operand + case NOT -> !evaluateOperand(left(expression), metadata); + case EQ -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) == 0; + case NE -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) != 0; + case GT -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) > 0; + case GTE -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) >= 0; + case LT -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) < 0; + case LTE -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) <= 0; + case IN -> { + Object metaVal = metadataValue(left(expression), metadata); + List<?> list = asList(filterValue(right(expression)), expression); + yield list.stream().anyMatch(item -> compare(metaVal, item) == 0); + } + case NIN -> { + Object metaVal = metadataValue(left(expression), metadata); + List<?> list = asList(filterValue(right(expression)), expression); + yield list.stream().noneMatch(item -> compare(metaVal, item) == 0); + } + // Unary operators: only the left operand (the key) is used. + // A non-null right operand is silently ignored here. + case ISNULL -> metadataValue(left(expression), metadata) == null; + case ISNOTNULL -> metadataValue(left(expression), metadata) != null; + }; + } + + private Filter.Operand left(Filter.Expression expression) { + Filter.Operand left = expression.left(); + if (left == null) { + throw new IllegalArgumentException( + "Expression of type %s requires a left operand".formatted(expression.type())); + } + return left; + } + + private Filter.Operand right(Filter.Expression expression) { + Filter.Operand right = expression.right(); + if (right == null) { + throw new IllegalArgumentException( + "Expression of type %s requires a right operand".formatted(expression.type())); + } + return right; + } + + /** + * Extracts the metadata value for the given {@link Filter.Key} operand. Outer quotes + * ({@code "..."} or {@code '...'}) are stripped from the key name to match the format + * used by {@link FilterExpressionBuilder} and the text parser. + */ + private @Nullable Object metadataValue(Filter.Operand operand, Map<String, Object> metadata) { + if (operand instanceof Filter.Key key) { + String k = key.key(); + if (k.length() >= 2 + && ((k.startsWith("\"") && k.endsWith("\"")) || (k.startsWith("'") && k.endsWith("'")))) { + k = k.substring(1, k.length() - 1); + } + return metadata.get(k); + } + throw new IllegalArgumentException("Expected a Key operand but got: " + operand.getClass().getName()); + } + + /** + * Extracts the constant value from a {@link Filter.Value} operand. {@link Date} + * instances are formatted to their ISO-8601 UTC string so they can be compared + * directly with metadata strings stored in the same format. + */ + private Object filterValue(Filter.Operand operand) { + if (operand instanceof Filter.Value filterValue) { + Object value = filterValue.value(); + return (value instanceof Date date) ? DATE_FORMATTER.format(date.toInstant()) : value; + } + throw new IllegalArgumentException("Expected a Value operand but got: " + operand.getClass().getName()); + } + + /** + * Compares two values. Numbers are promoted to {@code double} to allow cross-type + * numeric comparison (e.g. {@code Integer} vs {@code Double}). All other + * {@link Comparable} types are compared directly. + * + * <p> + * Null ordering follows SQL {@code NULLS FIRST} semantics: {@code null} is considered + * less than any non-null value. As a result, a missing metadata key causes ordered + * comparisons ({@code GT}, {@code GTE}, {@code LT}, {@code LTE}) to behave as if the + * key holds the smallest possible value — e.g. {@code year > 2020} returns + * {@code false} when {@code year} is absent. + */ + @SuppressWarnings("unchecked") + private int compare(@Nullable Object metaVal, @Nullable Object filterVal) { + if (metaVal == null && filterVal == null) { + return 0; + } + if (metaVal == null) { + return -1; + } + if (filterVal == null) { + return 1; + } + if (metaVal instanceof Number n1 && filterVal instanceof Number n2) { + return Double.compare(n1.doubleValue(), n2.doubleValue()); + } + if (Objects.equals(metaVal, filterVal)) { + return 0; + } + if (metaVal instanceof Comparable comparable && filterVal instanceof Comparable) { + try { + return comparable.compareTo(filterVal); + } + catch (ClassCastException ex) { + throw new IllegalArgumentException("Cannot compare values of incompatible types %s and %s" + .formatted(metaVal.getClass().getName(), filterVal.getClass().getName()), ex); + } + } + throw new IllegalArgumentException("Cannot compare values of types %s and %s" + .formatted(metaVal.getClass().getName(), filterVal.getClass().getName())); + } + + private List<?> asList(Object value, Filter.Expression expression) { + if (value instanceof List<?> list) { + return list; + } + throw new IllegalArgumentException( + "Expected a List value for %s expression but got: %s".formatted(expression.type(), value)); + } + +}
spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SimpleVectorStore.java+25 −24 modified@@ -47,24 +47,34 @@ import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric; import org.springframework.ai.util.JacksonUtils; import org.springframework.ai.vectorstore.filter.Filter; -import org.springframework.ai.vectorstore.filter.FilterExpressionConverter; -import org.springframework.ai.vectorstore.filter.converter.SimpleVectorStoreFilterExpressionConverter; import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore; import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext; import org.springframework.core.io.Resource; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; /** - * SimpleVectorStore is a simple implementation of the VectorStore interface. + * A simple, in-memory implementation of the <a href= + * "https://docs.spring.io/spring-ai/reference/api/vectordbs.html#_understanding_vectors">VectorStore</a> + * interface. * - * It also provides methods to save the current state of the vectors to a file, and to - * load vectors from a file. + * <p> + * Uses a {@link java.util.concurrent.ConcurrentHashMap} to store vectors and their + * associated metadata. Map keys are document IDs; values are + * {@link SimpleVectorStoreContent} instances that encapsulate each document's text, + * metadata, and embedding vector. * - * For a deeper understanding of the mathematical concepts and computations involved in - * calculating similarity scores among vectors, refer to this - * [resource](https://docs.spring.io/spring-ai/reference/api/vectordbs.html#_understanding_vectors). + * <p> + * Similarity search is performed using cosine similarity over all stored vectors. Filter + * expressions on document metadata are evaluated via + * {@link SimpleVectorStoreFilterExpressionEvaluator}. + * + * <p> + * The store can be persisted to and restored from a JSON file via the + * {@link #save(java.io.File)} and {@link #load(java.io.File)} / + * {@link #load(org.springframework.core.io.Resource)} methods. + * + * <p> + * <b>NOTE</b>: This implementation is not designed for production use and should only be + * used for testing or demonstration purposes. * * @author Raphael Yu * @author Dingmeng Xue @@ -75,24 +85,22 @@ * @author Thomas Vitale * @author Jemin Huh * @author David Yu + * @since 1.0.0 */ public class SimpleVectorStore extends AbstractObservationVectorStore { private static final Logger logger = LoggerFactory.getLogger(SimpleVectorStore.class); private final JsonMapper jsonMapper; - private final ExpressionParser expressionParser; - - private final FilterExpressionConverter filterExpressionConverter; + private final SimpleVectorStoreFilterExpressionEvaluator filterExpressionEvaluator; protected Map<String, SimpleVectorStoreContent> store = new ConcurrentHashMap<>(); protected SimpleVectorStore(SimpleVectorStoreBuilder builder) { super(builder); this.jsonMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build(); - this.expressionParser = new SpelExpressionParser(); - this.filterExpressionConverter = new SimpleVectorStoreFilterExpressionConverter(); + this.filterExpressionEvaluator = new SimpleVectorStoreFilterExpressionEvaluator(); } /** @@ -154,14 +162,7 @@ private Predicate<SimpleVectorStoreContent> doFilterPredicate(Filter.@Nullable E if (filterExpression == null) { return document -> true; } - - return document -> { - StandardEvaluationContext context = new StandardEvaluationContext(); - context.setVariable("metadata", document.getMetadata()); - return Boolean.TRUE.equals(this.expressionParser - .parseExpression(this.filterExpressionConverter.convertExpression(filterExpression)) - .getValue(context, Boolean.class)); - }; + return document -> this.filterExpressionEvaluator.evaluate(filterExpression, document.getMetadata()); } /**
spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/filter/converter/SimpleVectorStoreFilterExpressionConverterTests.java+0 −220 removed@@ -1,220 +0,0 @@ -/* - * Copyright 2023-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.ai.vectorstore.filter.converter; - -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.stream.IntStream; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import org.springframework.ai.vectorstore.filter.Filter; -import org.springframework.ai.vectorstore.filter.FilterExpressionConverter; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.AND; -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.EQ; -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.GTE; -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.IN; -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.LTE; -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NE; -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NIN; -import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.OR; - -/** - * @author Jemin Huh - */ -public class SimpleVectorStoreFilterExpressionConverterTests { - - final FilterExpressionConverter converter = new SimpleVectorStoreFilterExpressionConverter(); - - @Test - public void testDate() { - String vectorExpr = this.converter.convertExpression(new Filter.Expression(EQ, new Filter.Key("activationDate"), - new Filter.Value(new Date(1704637752148L)))); - assertThat(vectorExpr).isEqualTo("#metadata['activationDate'] == '2024-01-07T14:29:12Z'"); - - StandardEvaluationContext context = new StandardEvaluationContext(); - ExpressionParser parser = new SpelExpressionParser(); - context.setVariable("metadata", - Map.of("activationDate", "2024-01-07T14:29:12Z", "year", 2020, "country", "BG")); - Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class)); - - vectorExpr = this.converter.convertExpression( - new Filter.Expression(EQ, new Filter.Key("activationDate"), new Filter.Value("1970-01-01T00:00:02Z"))); - assertThat(vectorExpr).isEqualTo("#metadata['activationDate'] == '1970-01-01T00:00:02Z'"); - - context.setVariable("metadata", - Map.of("activationDate", "1970-01-01T00:00:02Z", "year", 2020, "country", "BG")); - Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class)); - - } - - @Test - public void testDatesConcurrently() { - IntStream.range(0, 10).parallel().forEach(i -> { - String vectorExpr = this.converter.convertExpression(new Filter.Expression(EQ, - new Filter.Key("activationDate"), new Filter.Value(new Date(1704637752148L)))); - String vectorExpr2 = this.converter.convertExpression(new Filter.Expression(EQ, - new Filter.Key("activationDate"), new Filter.Value(new Date(1704637753150L)))); - assertThat(vectorExpr).isEqualTo("#metadata['activationDate'] == '2024-01-07T14:29:12Z'"); - assertThat(vectorExpr2).isEqualTo("#metadata['activationDate'] == '2024-01-07T14:29:13Z'"); - }); - } - - @Test - public void testEQ() { - String vectorExpr = this.converter - .convertExpression(new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG"))); - assertThat(vectorExpr).isEqualTo("#metadata['country'] == 'BG'"); - - StandardEvaluationContext context = new StandardEvaluationContext(); - ExpressionParser parser = new SpelExpressionParser(); - context.setVariable("metadata", Map.of("city", "Seoul", "year", 2020, "country", "BG")); - Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class)); - - } - - @Test - public void tesEqAndGte() { - String vectorExpr = this.converter.convertExpression(new Filter.Expression(AND, - new Filter.Expression(EQ, new Filter.Key("genre"), new Filter.Value("drama")), - new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)))); - assertThat(vectorExpr).isEqualTo("#metadata['genre'] == 'drama' and #metadata['year'] >= 2020"); - - StandardEvaluationContext context = new StandardEvaluationContext(); - ExpressionParser parser = new SpelExpressionParser(); - context.setVariable("metadata", Map.of("genre", "drama", "year", 2020, "country", "BG")); - Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class)); - - } - - @Test - public void tesIn() { - String vectorExpr = this.converter.convertExpression(new Filter.Expression(IN, new Filter.Key("genre"), - new Filter.Value(List.of("comedy", "documentary", "drama")))); - assertThat(vectorExpr).isEqualTo("{'comedy','documentary','drama'}.contains(#metadata['genre'])"); - - StandardEvaluationContext context = new StandardEvaluationContext(); - ExpressionParser parser = new SpelExpressionParser(); - context.setVariable("metadata", Map.of("genre", "drama", "year", 2020, "country", "BG")); - Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class)); - - } - - @Test - public void testNe() { - String vectorExpr = this.converter.convertExpression( - new Filter.Expression(OR, new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)), - new Filter.Expression(AND, - new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG")), - new Filter.Expression(NE, new Filter.Key("city"), new Filter.Value("Sofia"))))); - assertThat(vectorExpr) - .isEqualTo("#metadata['year'] >= 2020 or #metadata['country'] == 'BG' and #metadata['city'] != 'Sofia'"); - - StandardEvaluationContext context = new StandardEvaluationContext(); - ExpressionParser parser = new SpelExpressionParser(); - context.setVariable("metadata", Map.of("city", "Seoul", "year", 2020, "country", "BG")); - Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class)); - - } - - @Test - public void testGroup() { - String vectorExpr = this.converter.convertExpression(new Filter.Expression(AND, - new Filter.Group(new Filter.Expression(OR, - new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)), - new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG")))), - new Filter.Expression(NIN, new Filter.Key("city"), new Filter.Value(List.of("Sofia", "Plovdiv"))))); - assertThat(vectorExpr).isEqualTo( - "(#metadata['year'] >= 2020 or #metadata['country'] == 'BG') and not {'Sofia','Plovdiv'}.contains(#metadata['city'])"); - - StandardEvaluationContext context = new StandardEvaluationContext(); - ExpressionParser parser = new SpelExpressionParser(); - context.setVariable("metadata", Map.of("city", "Seoul", "year", 2020, "country", "BG")); - Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class)); - - } - - @Test - public void tesBoolean() { - String vectorExpr = this.converter.convertExpression(new Filter.Expression(AND, - new Filter.Expression(AND, new Filter.Expression(EQ, new Filter.Key("isOpen"), new Filter.Value(true)), - new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020))), - new Filter.Expression(IN, new Filter.Key("country"), new Filter.Value(List.of("BG", "NL", "US"))))); - - assertThat(vectorExpr).isEqualTo( - "#metadata['isOpen'] == true and #metadata['year'] >= 2020 and {'BG','NL','US'}.contains(#metadata['country'])"); - - StandardEvaluationContext context = new StandardEvaluationContext(); - ExpressionParser parser = new SpelExpressionParser(); - context.setVariable("metadata", Map.of("isOpen", true, "year", 2020, "country", "NL")); - Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class)); - - vectorExpr = this.converter.convertExpression(new Filter.Expression(AND, - new Filter.Expression(AND, new Filter.Expression(EQ, new Filter.Key("isOpen"), new Filter.Value(true)), - new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020))), - new Filter.Expression(NIN, new Filter.Key("country"), new Filter.Value(List.of("BG", "NL", "US"))))); - - assertThat(vectorExpr).isEqualTo( - "#metadata['isOpen'] == true and #metadata['year'] >= 2020 and not {'BG','NL','US'}.contains(#metadata['country'])"); - - context.setVariable("metadata", Map.of("isOpen", true, "year", 2020, "country", "KR")); - Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class)); - } - - @Test - public void testDecimal() { - String vectorExpr = this.converter.convertExpression(new Filter.Expression(AND, - new Filter.Expression(GTE, new Filter.Key("temperature"), new Filter.Value(-15.6)), - new Filter.Expression(LTE, new Filter.Key("temperature"), new Filter.Value(20.13)))); - - assertThat(vectorExpr).isEqualTo("#metadata['temperature'] >= -15.6 and #metadata['temperature'] <= 20.13"); - - StandardEvaluationContext context = new StandardEvaluationContext(); - ExpressionParser parser = new SpelExpressionParser(); - context.setVariable("metadata", Map.of("temperature", -15.6)); - Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class)); - context.setVariable("metadata", Map.of("temperature", 20.13)); - Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class)); - context.setVariable("metadata", Map.of("temperature", -1.6)); - Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class)); - - } - - @Test - public void testComplexIdentifiers() { - String vectorExpr = this.converter - .convertExpression(new Filter.Expression(EQ, new Filter.Key("\"country 1 2 3\""), new Filter.Value("BG"))); - assertThat(vectorExpr).isEqualTo("#metadata['country 1 2 3'] == 'BG'"); - - vectorExpr = this.converter - .convertExpression(new Filter.Expression(EQ, new Filter.Key("'country 1 2 3'"), new Filter.Value("BG"))); - assertThat(vectorExpr).isEqualTo("#metadata['country 1 2 3'] == 'BG'"); - - StandardEvaluationContext context = new StandardEvaluationContext(); - ExpressionParser parser = new SpelExpressionParser(); - context.setVariable("metadata", Map.of("country 1 2 3", "BG")); - Assertions.assertEquals(Boolean.TRUE, parser.parseExpression(vectorExpr).getValue(context, Boolean.class)); - } - -}
spring-ai-vector-store/src/test/java/org/springframework/ai/vectorstore/SimpleVectorStoreFilterExpressionEvaluatorTests.java+273 −0 added@@ -0,0 +1,273 @@ +/* + * Copyright 2023-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.vectorstore; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.ai.vectorstore.filter.Filter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.AND; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.EQ; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.GT; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.GTE; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.IN; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.ISNOTNULL; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.ISNULL; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.LT; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.LTE; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NE; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NIN; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.NOT; +import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.OR; + +/** + * Tests for {@link SimpleVectorStoreFilterExpressionEvaluator}. + * + * @author Christian Tzolov + */ +class SimpleVectorStoreFilterExpressionEvaluatorTests { + + private final SimpleVectorStoreFilterExpressionEvaluator evaluator = new SimpleVectorStoreFilterExpressionEvaluator(); + + // ------------------------------------------------------------------------- + // Comparison operators + // ------------------------------------------------------------------------- + + @Test + void testEq() { + var expr = new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG")); + assertThat(this.evaluator.evaluate(expr, Map.of("country", "BG"))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("country", "NL"))).isFalse(); + } + + @Test + void testNe() { + var expr = new Filter.Expression(NE, new Filter.Key("country"), new Filter.Value("BG")); + assertThat(this.evaluator.evaluate(expr, Map.of("country", "NL"))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("country", "BG"))).isFalse(); + } + + @Test + void testGt() { + var expr = new Filter.Expression(GT, new Filter.Key("year"), new Filter.Value(2020)); + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2021))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isFalse(); + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2019))).isFalse(); + } + + @Test + void testGte() { + var expr = new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)); + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2021))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2019))).isFalse(); + } + + @Test + void testLt() { + var expr = new Filter.Expression(LT, new Filter.Key("year"), new Filter.Value(2020)); + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2019))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isFalse(); + } + + @Test + void testLte() { + var expr = new Filter.Expression(LTE, new Filter.Key("year"), new Filter.Value(2020)); + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2019))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2021))).isFalse(); + } + + // ------------------------------------------------------------------------- + // Logical operators + // ------------------------------------------------------------------------- + + @Test + void testAnd() { + var expr = new Filter.Expression(AND, + new Filter.Expression(EQ, new Filter.Key("genre"), new Filter.Value("drama")), + new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020))); + + assertThat(this.evaluator.evaluate(expr, Map.of("genre", "drama", "year", 2020))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("genre", "comedy", "year", 2020))).isFalse(); + assertThat(this.evaluator.evaluate(expr, Map.of("genre", "drama", "year", 2019))).isFalse(); + } + + @Test + void testOr() { + var expr = new Filter.Expression(OR, new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)), + new Filter.Expression(AND, new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG")), + new Filter.Expression(NE, new Filter.Key("city"), new Filter.Value("Sofia")))); + + assertThat(this.evaluator.evaluate(expr, Map.of("city", "Seoul", "year", 2020, "country", "BG"))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("city", "Seoul", "year", 2019, "country", "BG"))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("city", "Sofia", "year", 2019, "country", "BG"))).isFalse(); + } + + @Test + void testNot() { + var expr = new Filter.Expression(NOT, + new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG"))); + assertThat(this.evaluator.evaluate(expr, Map.of("country", "NL"))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("country", "BG"))).isFalse(); + } + + // ------------------------------------------------------------------------- + // Collection operators + // ------------------------------------------------------------------------- + + @Test + void testIn() { + var expr = new Filter.Expression(IN, new Filter.Key("genre"), + new Filter.Value(List.of("comedy", "documentary", "drama"))); + assertThat(this.evaluator.evaluate(expr, Map.of("genre", "drama"))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("genre", "comedy"))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("genre", "action"))).isFalse(); + } + + @Test + void testNin() { + var expr = new Filter.Expression(NIN, new Filter.Key("city"), new Filter.Value(List.of("Sofia", "Plovdiv"))); + assertThat(this.evaluator.evaluate(expr, Map.of("city", "Seoul"))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("city", "Sofia"))).isFalse(); + } + + // ------------------------------------------------------------------------- + // Null checks + // ------------------------------------------------------------------------- + + @Test + void testIsNull() { + var expr = new Filter.Expression(ISNULL, new Filter.Key("country")); + Map<String, Object> withNull = new java.util.HashMap<>(); + withNull.put("country", null); + assertThat(this.evaluator.evaluate(expr, withNull)).isTrue(); + // missing key → null + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("country", "BG"))).isFalse(); + } + + @Test + void testIsNotNull() { + var expr = new Filter.Expression(ISNOTNULL, new Filter.Key("country")); + assertThat(this.evaluator.evaluate(expr, Map.of("country", "BG"))).isTrue(); + // missing key → null + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isFalse(); + } + + // ------------------------------------------------------------------------- + // Group (precedence) + // ------------------------------------------------------------------------- + + @Test + void testGroup() { + // (year >= 2020 OR country == "BG") AND city NIN ["Sofia", "Plovdiv"] + var expr = new Filter.Expression(AND, + new Filter.Group(new Filter.Expression(OR, + new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)), + new Filter.Expression(EQ, new Filter.Key("country"), new Filter.Value("BG")))), + new Filter.Expression(NIN, new Filter.Key("city"), new Filter.Value(List.of("Sofia", "Plovdiv")))); + + assertThat(this.evaluator.evaluate(expr, Map.of("city", "Seoul", "year", 2020, "country", "BG"))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("city", "Sofia", "year", 2020, "country", "BG"))).isFalse(); + assertThat(this.evaluator.evaluate(expr, Map.of("city", "Seoul", "year", 2019, "country", "NL"))).isFalse(); + } + + // ------------------------------------------------------------------------- + // Type handling + // ------------------------------------------------------------------------- + + @Test + void testBoolean() { + // isOpen == true AND year >= 2020 AND country IN ["BG", "NL", "US"] + var expr = new Filter.Expression(AND, + new Filter.Expression(AND, new Filter.Expression(EQ, new Filter.Key("isOpen"), new Filter.Value(true)), + new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020))), + new Filter.Expression(IN, new Filter.Key("country"), new Filter.Value(List.of("BG", "NL", "US")))); + + assertThat(this.evaluator.evaluate(expr, Map.of("isOpen", true, "year", 2020, "country", "NL"))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("isOpen", false, "year", 2020, "country", "NL"))).isFalse(); + assertThat(this.evaluator.evaluate(expr, Map.of("isOpen", true, "year", 2019, "country", "NL"))).isFalse(); + assertThat(this.evaluator.evaluate(expr, Map.of("isOpen", true, "year", 2020, "country", "KR"))).isFalse(); + } + + @Test + void testDecimal() { + var expr = new Filter.Expression(AND, + new Filter.Expression(GTE, new Filter.Key("temperature"), new Filter.Value(-15.6)), + new Filter.Expression(LTE, new Filter.Key("temperature"), new Filter.Value(20.13))); + + assertThat(this.evaluator.evaluate(expr, Map.of("temperature", -15.6))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("temperature", 20.13))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("temperature", -1.6))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("temperature", -16.0))).isFalse(); + assertThat(this.evaluator.evaluate(expr, Map.of("temperature", 21.0))).isFalse(); + } + + @Test + void testNumericCrossTypeComparison() { + // metadata Integer vs filter Integer — should work via double promotion + var expr = new Filter.Expression(GTE, new Filter.Key("year"), new Filter.Value(2020)); + assertThat(this.evaluator.evaluate(expr, Map.of("year", 2020))).isTrue(); + // metadata Integer vs filter Double + assertThat(this.evaluator.evaluate(expr, Map.of("year", Integer.valueOf(2020)))).isTrue(); + } + + @Test + void testInNumericCrossType() { + // metadata Integer, filter list contains Long — must match via double promotion + var expr = new Filter.Expression(IN, new Filter.Key("year"), new Filter.Value(List.of(2019L, 2020L, 2021L))); + assertThat(this.evaluator.evaluate(expr, Map.of("year", Integer.valueOf(2020)))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("year", Integer.valueOf(2022)))).isFalse(); + + // metadata Double, filter list contains Integer + var expr2 = new Filter.Expression(IN, new Filter.Key("score"), new Filter.Value(List.of(1, 2, 3))); + assertThat(this.evaluator.evaluate(expr2, Map.of("score", 2.0))).isTrue(); + assertThat(this.evaluator.evaluate(expr2, Map.of("score", 4.0))).isFalse(); + } + + @Test + void testNinNumericCrossType() { + // metadata Long, filter list contains Integer + var expr = new Filter.Expression(NIN, new Filter.Key("year"), new Filter.Value(List.of(2020, 2021))); + assertThat(this.evaluator.evaluate(expr, Map.of("year", Long.valueOf(2022)))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("year", Long.valueOf(2020)))).isFalse(); + } + + @Test + void testDate() { + var expr = new Filter.Expression(EQ, new Filter.Key("activationDate"), + new Filter.Value(new Date(1704637752148L))); + assertThat(this.evaluator.evaluate(expr, Map.of("activationDate", "2024-01-07T14:29:12Z"))).isTrue(); + assertThat(this.evaluator.evaluate(expr, Map.of("activationDate", "2024-01-07T00:00:00Z"))).isFalse(); + } + + @Test + void testQuotedKey() { + var exprDoubleQuote = new Filter.Expression(EQ, new Filter.Key("\"country 1 2 3\""), new Filter.Value("BG")); + assertThat(this.evaluator.evaluate(exprDoubleQuote, Map.of("country 1 2 3", "BG"))).isTrue(); + + var exprSingleQuote = new Filter.Expression(EQ, new Filter.Key("'country 1 2 3'"), new Filter.Value("BG")); + assertThat(this.evaluator.evaluate(exprSingleQuote, Map.of("country 1 2 3", "BG"))).isTrue(); + } + +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-fvh3-672c-7p6cghsaADVISORY
- spring.io/security/cve-2026-22738nvdVendor Advisory
- github.com/spring-projects/spring-ai/commit/ba9220b22383e430d5f801ce8e4fa01cf9e75f29ghsa
- github.com/spring-projects/spring-ai/releases/tag/v1.0.5ghsa
- github.com/spring-projects/spring-ai/releases/tag/v1.1.4ghsa
- nvd.nist.gov/vuln/detail/CVE-2026-22738ghsa
News mentions
0No linked articles in our index yet.