Key Takeaway: CVE-2026-32710 is a severe heap-based buffer overflow in MariaDB Server’s JSON_SCHEMA_VALID() function that can crash the database. In tightly controlled conditions, it may enable remote code execution. As part of the OPSWAT Cybersecurity Fellowship Program, our fellows conducted a technical analysis of CVE-2026-32710.
MariaDB Server versions 11.4.1–11.4.9, 11.8.1–11.8.5, and 12.1.2 contain a heap-based buffer overflow (CWE-122) in JSON Schema validation. An authenticated database user can trigger the flaw through JSON_SCHEMA_VALID(), causing memory corruption and a server crash. Public advisories describe remote code execution as possible only under tightly controlled memory-layout conditions, generally attainable in a laboratory.
What follows is a technical analysis of CVE-2026-32710 and how its exploitation could affect MariaDB's JSON Schema Validation, conduced by Khoa Bui Le Anh and Minh Pham Nhat, as part of the OPSWAT Cybersecurity Fellowship Program/

Classification
The NVD lists a CVSS v3.1 score of 9.9 (Critical); its record also includes a separate CNA assessment of 8.5 (High). The differing attack-complexity ratings reflect different assessments of how practical exploitation is.

About Maria DB
MariaDB is an open-source relational database created by the original developers of MySQL, widely used in Linux, cloud, hosting, and enterprise environments. It handles JSON by storing it as text (an alias for LONGTEXT) and offers functions to work with it, including JSON_SCHEMA_VALID(), which checks a JSON document against a predefined schema.
A heap-based buffer overflow in this function, tracked as CVE-2026-32710, can crash the server and, under specific conditions, may allow remote code execution.
Contesto tecnico
JSON Schema Validation in MariaDB
A syntactically valid JSON document does not necessarily match the structure an application expects. MariaDB’s JSON_VALID() function only checks whether a value contains valid JSON syntax. Applications may also need to enforce required properties, expected data types, numerical limits, array uniqueness, or allowed values.
For that purpose, MariaDB provides JSON_SCHEMA_VALID(schema, json), which evaluates a JSON document against a supplied JSON Schema. The function returns 1 when the document satisfies the schema and 0 when validation fails.

User-Defined Variables
MariaDB supports user-defined variables that allow values to be stored and reused across SQL statements within the same database session. These variables are prefixed with @ and remain available throughout the session. For example:

The first statement stores "red" in @color. Later statements in the same session can read that value.
Internally, MariaDB stores each variable in a user_var_entry structure. The structure contains metadata for the variable and a pointer to its current value. The figure shows the full layout. For this analysis, the important fields are the value pointer and the value length.

When MariaDB executes SET @name = value;, Item_func_set_user_var::set_entry() calls get_variable().

If @name already exists in the session’s variable table, MariaDB reuses the existing user_var_entry. If it does not exist, MariaDB creates a new entry and allocates the user_var_entry structure on the heap. At that point, MariaDB has created the variable record, but it has not yet stored the assigned value. entry->value is NULL, and entry->length is zero.
MariaDB then evaluates the expression on the right side of = in Item_func_set_user_var::check(). After evaluating value, Item_func_set_user_var::update() calls update_hash().

For strings, update_hash() reserves one extra byte for the terminating \0.
The storage decision depends on the new string length:
- 7 bytes or fewer: MariaDB stores the value in the inline area. If the previous value used an external buffer, that buffer is released.
- 8 bytes or more: MariaDB stores the value in a separate heap buffer sized for the payload plus \0.
- During an update through SET, if the previous value was stored inline, MariaDB allocates a new external buffer. If the previous value already used an external buffer, my_realloc() resizes that allocation in place or moves it to a new heap address, depending on the requested size.
UDFs (User-Defined Functions)
MariaDB UDFs let developers extend SQL with functions implemented in a shared library. They are useful when an application needs behavior that MariaDB does not provide through built-in functions.
A UDF is registered with CREATE FUNCTION and can then be called like a regular SQL function:

The shared library must be placed in MariaDB’s configured plugin directory. This extension mechanism becomes important later, because the exploit chain redirects plugin_dir and then loads an attacker-controlled shared object.
Heap Allocation and Reuse
In the Linux environment used for this analysis, MariaDB uses glibc’s malloc() for heap allocation. The vulnerable json_get_normalized_string() path initializes a temporary buffer with a capacity of 128 bytes. After glibc adds allocator metadata and alignment, that allocation falls into the 0x90 heap-chunk size class. The heap analysis therefore focuses on 0x90 chunks, because reusing a chunk of that size can place the vulnerable buffer beside a chosen user_var_entry object.
When glibc allocates another chunk in this size class, it first checks the thread-local cache (tcache). Tcache stores recently freed chunks in a singly linked list and reuses them in last-in, first-out order. In the figure, EEE, FFF, and ZZZ represent free 0x90 chunks. If ZZZ is the list head, consecutive allocations reuse ZZZ, then FFF, and then EEE.

If no compatible chunk is available, glibc searches other reusable bins. Depending on allocator state, those may include fastbins, small bins, or the unsorted bin. If no suitable free chunk exists, glibc obtains memory from the top chunk, which is the unused free space at the end of the current heap arena.
Functions, variables, rules | Scopo | Key details |
JSON_VALID() | Checks JSON syntax only | Doesn't verify structure, types, or required fields |
JSON_SCHEMA_VALID(schema, json) | Checks a JSON document against a JSON Schema | Enforces required properties, data types, numeric limits, array uniqueness, allowed values. Returns 1 if valid, 0 if not |
User-defined variables | Session-scoped storage for values, prefixed with @ | Persist across statements in the same session (e.g., @color = 'red') |
user_var_entry | Internal heap structure holding each variable | Key fields: a value pointer and a length. A new entry starts with value = NULL, length = 0 |
SET @name = value | Assignment flow | set_entry() → get_variable() (reuse or create entry) → check() (evaluate the right side) → update() → update_hash() (store the value) |
String storage rules | Where the value lives, based on length (+1 byte for \0) | ≤ 7 bytes: stored inline, any old external buffer is freed. ≥ 8 bytes: stored in a separate heap buffer. |
Updating a stored string | Memory behavior on reassignment | Previous value inline → allocate a new external buffer. Previous value external → my_realloc() resizes in place or moves it to a new heap address |
User-Defined Functions (UDFs) | Custom SQL functions implemented in a shared library | Registered with CREATE FUNCTION, called like built-in functions |
Plugin directory | Where UDF shared libraries must live | Relevant later: the exploit chain redirects plugin_dir and loads an attacker-controlled shared object |
Why It Matters for the Exploit
User variables give an attacker a way to control heap allocations (size, timing, and reallocation behavior), and UDFs provide the route to turn memory corruption into code execution by loading a malicious library.
Internal Execution Flow
JSON_SCHEMA_VALID() evaluation flow
JSON_SCHEMA_VALID() operates in two phases.
MariaDB prepares the schema once per statement: it parses the first argument and builds a validator for each schema keyword.
Since, in the current representation, the schema must be constant, those validators are reused. The second argument, the document, is read every time the function runs, so it can be a column value or changing expression, and each row is validated separately.
The function returns 1 when the document satisfies the schema and 0 when it does not. For example, the following query returns 1 because "red" is one of the permitted values:

Schema Preparation
Step 1) Creating the schema validators
MariaDB begins in fix_length_and_dec(), which prepares the constant schema argument before it evaluates the document. After initializing the JSON parser, it calls create_object_and_handle_keyword() for each schema keyword. Each recognized keyword creates a validator and adds it to keyword_list for later document validation.

MariaDB uses specialized validators for keywords such as enum, const, and uniqueItems. For example, enum creates a Json_schema_enum validator that stores the permitted values for later comparison.
Step 2) Preparing values for document validation
create_object_and_handle_keyword() processes each keyword and its associated JSON value. create_object() selects the validator based on the keyword name, then calls that validator’s handle_keyword() method.

json_read_value() performs generic JSON parsing and records the value’s type and contents. Each validator’s handle_keyword() method then stores the parsed value in the form needed for later comparison.
Document Validation
Step 1) Starting document validation
Document processing in val_bool() begins only after fix_length_and_dec() successfully parses the schema and creates its validators. If schema_parsed is false, val_bool() returns immediately without reading the document.

If schema_parsed is true, val_bool() reads the second argument, which is the JSON document being checked. It initializes a JSON parser, calls json_read_value() to read the document’s top-level value, and passes that value to each validator in keyword_list.
Step 2) Parsing the JSON string
json_read_value() first skips leading whitespace using get_first_nonspace() and examines the first non-whitespace character to determine the JSON value type and select the appropriate parser.
For example, when processing the JSON string "database_admin", the first character is a quotation mark. Based on this character, the JST_READ_VALUE entry in json_actions selects read_strn() to process the input as a JSON string.

The read_strn() function initializes the parser state by setting value_type to JSON_VALUE_STRING and recording the starting position of the string content. It then calls skip_str_constant() to validate the string syntax.

The skip_str_constant() function processes the string character by character through json_next_char(). It continues until it encounters a closing quotation mark or detects an invalid character or escape sequence. When a backslash is encountered, the function sets value_escaped and invokes json_handle_esc() to validate the escape sequence.

These checks implement MariaDB's JSON string parsing behavior based on RFC 8259, Section 7 [1]. The following table summarizes valid and invalid JSON string examples.
Condition | Esempio | Risultato |
Unescaped control character (U+0000–U+001F) | Literal newline or tab inside a string | Not allowed |
Unescaped quotation mark within a JSON string | "admin"account" | Not allowed |
Escaped quotation mark | "admin\"account" | Allowed |
Valid escape sequence | "admin\naccount" | Allowed |
Printable unescaped characters | "database_admin" | Allowed |
Well-formed Unicode escape | "\u0041" | Allowed |
Malformed Unicode escape | "\u12G4" | Not allowed |
Missing closing quotation mark | "database_admin | Not allowed |
If parsing succeeds, read_strn() calculates value_len as the byte length of the string content in the original JSON input, excluding the surrounding quotation marks. For example, "database_admin" produces a value_len of 14 bytes, while "\u0041" produces a value_len of 6 bytes, even though the escape sequence represents a single character.
On success, json_read_value() returns 0 and preserves the parsed value's type, position, boundaries, and length in the parser state for subsequent processing. A nonzero return value indicates a parsing failure.
Step 3) Applying the schema validators
After parsing the JSON document, val_bool() invokes the validators stored in keyword_list. Each validator implements a validate() method to enforce the constraints associated with its schema keyword. Some validators perform direct checks, while others normalize JSON values before comparison.
The normalization helper is used by several validators, including enum, const, and uniqueItems. This analysis focuses on Json_schema_enum::validate(), which provides a direct path to the vulnerable normalization function.
For the enum keyword, Json_schema_enum::validate() handles the JSON literal values true, false, and null using flags prepared during schema processing. Other JSON value types, including strings, numbers, arrays, and objects, are passed to json_get_normalized_string() before being compared against the stored enum values.

The validator uses an internal Boolean return convention:
- false indicates that the value satisfies the validator.
- true indicates validation failure or an internal error.
Under normal validation conditions, successful validation contributes to a JSON_SCHEMA_VALID() result of 1, while a schema mismatch results in 0.
Importantly, normalization occurs before the enum membership check. Therefore, even a JSON string that does not match any permitted enum value can reach the vulnerable normalization function.
Analisi delle vulnerabilità
Causa principale
The vulnerability originates in json_get_normalized_string(), where a JSON string is copied into a temporary heap buffer without verifying that the destination has sufficient capacity.
The function initializes a DYNAMIC_STRING with a default allocation of 128 bytes. However, when processing a JSON string, it copies je->value_len bytes directly into the allocated buffer without resizing it first.
If the input length exceeds the available capacity, the copy writes beyond the allocated buffer, resulting in a heap-based buffer overflow.
Vulnerable Code: String Normalization
During enum validation, Json_schema_enum::validate() calls json_get_normalized_string() to normalize the candidate value before comparing it against the permitted enum values.
Normalization provides a consistent representation of JSON values, allowing equivalent values to be compared even when their original textual representations differ.
Examples include:
- Number: 0 -> 0.0E0
- Array: [1, 0] -> [1.0E0,0.0E0]
- Object: {"b":2,"a":1} -> {"a":1.0E0,"b":2.0E0}
The function receives the parsed JSON value through je, writes the normalized output to res, and uses error to indicate whether normalization succeeded. Internally, it initializes a_res, a temporary DYNAMIC_STRING backed by a heap allocation.

Numbers, arrays, and objects are processed through json_normalize(), which can expand the destination buffer as needed.
However, JSON strings follow a different execution path. The function copies je->value_len bytes directly into a_res.str using strncpy(), without first verifying or expanding the destination buffer's capacity.
The subsequent assignment, a_res.length = je->value_len, only updates the recorded string length. It does not resize the underlying allocation or prevent the preceding out-of-bounds write.
Consequently, when je->value_len exceeds the allocated buffer capacity, strncpy() writes beyond the heap allocation, potentially corrupting adjacent heap memory.
Exploitation: from Heap Buffer Overflow to Remote Code Execution
This section demonstrates how CVE-2026-32710 can be combined with two additional MariaDB issues, MDEV-40470 and MDEV-39581, to achieve remote code execution (RCE) in a controlled laboratory environment.
The exploit uses the heap buffer overflow to modify MariaDB's internal plugin_dir configuration, allowing a malicious User-Defined Function (UDF) to be loaded from a writable directory. Successful exploitation requires specific database privileges, filesystem conditions, and precise control over the heap layout. Therefore, RCE is not a guaranteed outcome of CVE-2026-32710 alone.
Step 1: Establishing the Required Database Privileges
MariaDB restricts file operations through two security mechanisms:
- Database privileges
Operations such as LOAD_FILE() and SELECT ... INTO DUMPFILE require the FILE privilege. - Filesystem permissions
MariaDB performs file operations under the operating-system account running mariadbd, which typically cannot write to the root-owned default plugin directory.
In addition, secure_file_priv can restrict file operations to a configured directory. By default, the variable is empty, so no additional directory restriction applies.

To overcome the database privilege restriction, the exploit first uses MDEV-40470 to modify the authentication configuration of root@localhost. Under the conditions required for this additional vulnerability, the attacker can authenticate as the privileged MariaDB account and obtain the database permissions needed for subsequent file operations.
This privilege escalation occurs within MariaDB and does not grant operating-system root access.

This provides the database privileges required for subsequent file operations but does not grant operating-system root access. The filesystem restriction on the default plugin directory is addressed later through the heap buffer overflow.
Step 2: Resolving Memory Addresses and Preparing the Heap
Address Space Layout Randomization (ASLR) prevents the use of fixed memory addresses. In the laboratory environment, the exploit uses the obtained database privileges to read the MariaDB executable and /proc/self/maps, allowing it to determine the runtime addresses of opt_plugin_dir and opt_plugin_dir_ptr despite ASLR.

Next, heap grooming is performed by creating 100 user-defined variables containing 127-byte values. These allocations use the same 0x90 size class as the temporary buffer allocated by json_get_normalized_string().

These allocations use the same 0x90 size class and consume reusable chunks of that size. Once those reusable chunks are exhausted, later allocations come from the top chunk, which makes the starting heap layout more predictable.

However, heap grooming alone does not reveal the addresses of the allocated objects. To obtain these addresses, MDEV-39581, an information disclosure vulnerability in COLUMN_LIST(), is used to leak heap memory.
The process begins by storing a malformed dynamic-column value in @src, followed by the creation of 127-byte user-defined variables.



Calling COLUMN_LIST() on the malformed value discloses approximately 65 KB of heap memory containing user_var_entry structures and their associated value buffers.

The leaked memory is analyzed to identify valid user_var_entry structures by checking their names, value lengths, pointer alignment, and surrounding structure fields. The exploit then selects three contiguous entries, referred to as entry_a, entry_b, and entry_c, with the required memory layout. Their addresses must also satisfy JSON string parsing constraints, since two address bytes are embedded in the subsequent overflow payload.

Step 3: Triggering the Heap Buffer Overflow
After identifying three suitable contiguous user_var_entry structures (entry_a, entry_b, and entry_c), the value buffer of entry_a is released while its variable entry remains allocated.


The temporary buffer allocated by json_get_normalized_string() then reuses the freed chunk. A crafted JSON string containing 192 filler bytes and two selected address bytes triggers the heap buffer overflow. The overflow overwrites only the lowest two bytes of entry_c.value.ptr, while preserving the remaining bytes of the original pointer. This partial pointer overwrite redirects entry_c.value.ptr to the targeted field within entry_a, enabling the subsequent memory manipulation.

Updating entry_c through the corrupted pointer changes entry_a.value.ptr to the address of opt_plugin_dir, establishing the memory-write capability needed to modify the plugin directory.

Step 4: Redirecting the Plugin Directory and Achieving RCE
The value length of entry_a is maintained at 9 bytes to preserve external storage rather than switching to inline storage. Updating entry_a then overwrites opt_plugin_dir with /tmp/.

With the plugin directory redirected to a writable location, a malicious shared library (raptor.so) is written to /tmp/ and registered using CREATE FUNCTION.
The library implements sys_exec(), which calls system() to execute operating-system commands with the privileges of the MariaDB service account.

Step 5: Restoring Heap Integrity
The heap buffer overflow also corrupts adjacent heap metadata and fields within entry_c, potentially causing MariaDB to crash during session cleanup.
To prevent this, the exploit uses a repair UDF to restore the corrupted heap metadata and structure fields using the original bytes obtained from the earlier memory disclosure.
Additionally, because entry_a.value.ptr was redirected to opt_plugin_dir, the exploit restores the pointer to its inline storage and resets entry_a.value.length to 0. The entry_b structure remains unchanged.

These repairs allow MariaDB to release the session resources without attempting to free invalid memory.
Prova di concetto
The full attack chain was successfully reproduced in an isolated MariaDB laboratory environment using the PoC script.
Our fellows started with a low-privileged authenticated SQL account and demonstrated the complete attack chain, including the JSON_SCHEMA_VALID() heap buffer overflow, plugin_dir overwrite, malicious UDF loading, and reverse-shell execution.

The reverse-shell callback confirmed operating-system command execution with the privileges of the MariaDB service account, demonstrating how the heap buffer overflow can lead to RCE when combined with the additional vulnerabilities and required environmental conditions.
Mitigazione
The primary mitigation for CVE-2026-32710 is to upgrade MariaDB Server to a patched version:
- MariaDB 11.4: Upgrade to 11.4.10 or later.
- MariaDB 11.8: Upgrade to 11.8.6 or later.
- MariaDB 12.2: Upgrade to 12.2.2 or later.
Organizations should also apply the following security measures to reduce the risk of exploitation:
- Restrict database privileges: Apply the principle of least privilege, particularly for the FILE privilege and UDF creation.
- Secure file operations: Configure secure_file_priv to restrict file access and prevent unauthorized modifications to the MariaDB plugin directory.
- Limit database exposure: Restrict database access to trusted users and systems, and monitor suspicious SQL activity.
Beyond patching, organizations can use OPSWAT’s MetaDefender™ solutions to improve vulnerability visibility and remediation:
- MetaDefender™ Core with SBOM analysis: Identify vulnerable MariaDB components and affected versions through Software Bill of Materials (SBOM) analysis, helping organizations assess exposure and prioritize updates.
- MetaDefender™ Tri with Watchlists: Proactively monitor MariaDB and other relevant products for newly disclosed or updated CVEs. Combined with vulnerability intelligence and the OPSWAT Heat Score, MetaDefender Tri helps security teams prioritize threats and take timely remediation action.

Together, SBOM-based component visibility and proactive CVE monitoring help organizations identify vulnerable software earlier and reduce exposure to emerging threats.
