DEV Community

GUIDANCE WHITE
GUIDANCE WHITE

Posted on

CVE-2026-92262: Nine Characters — "#mysql50#" — Is All It Takes to Break MariaDB (CVSS 9.9)

Overview

Item Detail
CVE ID CVE-2026-92262
Affected product MariaDB Server
Affected versions 10.6.1–10.6.27, 10.11.1–10.11.18, 11.4.1–11.4.12, 11.8.1–11.8.8, 12.3.1–12.3.2, 13.0.1
Fixed in 10.6.28, 10.11.19, 11.4.13, 11.8.9, 12.3.3, 13.0.2
CVSS 9.9 (Critical)
Type CWE-706 (Use of Incorrectly-Resolved Name or Reference) — a different category from the classic "path traversal" label
Official sources GHSA-65jj-h265-3m7h (MariaDB's official security advisory), MDEV-40362 (MariaDB's own JIRA bug)
Authentication required Yes — and at a very low bar. The public PoC reproduces this with nothing more than an ordinary ALL grant on your own schema (create tables, insert, select) — no FILE, no SUPER, no plugin-load privilege
Disclosed 2026-10-16, alongside the 10.6.28 and other patched releases
Note Disclosed the same day as two other 9.9-scored CVEs — CVE-2026-85748 (a SYS_REFCURSOR UAF) and CVE-2026-85746 (an ST_AsText OOB write) — all three bundled into a single PoC repository by the same researcher

Correction notice: An earlier version of this post described this flaw as classic path traversal (#mysql50#../../../etc/passwd-style). That characterization didn't match the official CWE classification (CWE-706, not CWE-22) or how the published PoC actually behaves. The public PoC demonstrates that a name containing ../ is explicitly rejected elsewhere with ERROR 1103 Incorrect table name. What follows has been rewritten based on the official advisory (GHSA-65jj-h265-3m7h), MariaDB's own bug tracker (MDEV-40362), and the published PoC code.

MariaDB has to run table names through a "translation step" before they become actual filesystem paths. SQL allows characters like - and @ in table names, but not every filesystem handles those characters safely. So MariaDB replaces "potentially unsafe" characters in a table name with a hex-coded form prefixed by @ (like @002d) when building the filename.

This CVE comes from a nearly 20-year-old compatibility feature that can skip that translation step entirely. A table name carrying the #mysql50# prefix is meant to reference "a table stored under MySQL 5.1-era encoding, as-is" — and to do that, it skips the encoding step altogether. An overt ../ sequence is blocked by a separate name-validity check, but a quieter problem remained underneath: a name with this prefix and the same name without it aren't properly distinguished inside the table cache.

Where the flaw lives — two names sharing one cache key

Here's the normal flow for turning a table name into a file in MariaDB:

  1. A user specifies a table name, as in CREATE TABLE t (...).
  2. The server encodes it for the filename character set via something like tablename_to_filename(). Characters like - and @ become @002d, @0040, and so on.
  3. At the same time, the server builds a table cache key to identify this table, so subsequent queries using the same name can reuse a cached handler instead of hitting disk every time.

The #mysql50# prefix was designed to affect step 2 (filename encoding). In practice, it also affects step 3 (cache key computation) — with the result that t and #mysql50#t, despite being different names, end up sharing the same cache entry and handler.

Step-by-step source analysis

Step 1 — the normal path: special characters are safely substituted

CREATE TABLE `a-b` (id INT) ENGINE=MyISAM;
Enter fullscreen mode Exit fullscreen mode

This table gets stored on disk as a@002db.frm, a@002db.MYD, a@002db.MYI. Because - was replaced with @002d, you can't tell from the filename alone that the original table name contained a - — but that substitution is exactly what keeps filesystem-ambiguous characters from ever appearing literally in a path.

Step 2 — "#mysql50#" skips that encoding entirely

CREATE TABLE `#mysql50#a-b` (id INT) ENGINE=MyISAM;
Enter fullscreen mode Exit fullscreen mode

According to what the public PoC actually confirms, running this command creates the following files in the data directory:

a-b.frm
a-b.MYD
a-b.MYI
Enter fullscreen mode Exit fullscreen mode

a@002db.frm never gets created. The - stayed unencoded, literally, in the filename. But the PoC repository also makes an important point here — putting ../ in the table name is rejected by a separate check, before it ever gets this far, with ERROR 1103 Incorrect table name. So this isn't the classic "escape to an arbitrary path" kind of flaw — it's the narrower, but still real, problem of "a character that should have been encoded ends up in the filename unencoded."

Step 3 — the root cause: the table cache confuses t and #mysql50#t (CWE-706)

The real core of this flaw isn't on disk at all — it's in the in-memory table cache. The reproduction steps recorded in MDEV-40362 look like this:

CREATE TABLE t (id INT, c1 BINARY(10)) ENGINE=MEMORY;
INSERT INTO t VALUES (1, REPEAT(0x41, 10));
SELECT id, HEX(c1) FROM t;
-- id=1  hex=41414141414141414141   (0x41, ten bytes, exactly as inserted)

DROP TABLE IF EXISTS `#mysql50#t`;
CREATE TABLE `#mysql50#t` (id INT, c1 BINARY(1)) ENGINE=MEMORY;
INSERT INTO `#mysql50#t` VALUES (2, 0x42);
SELECT id, HEX(c1) FROM `#mysql50#t` WHERE id=2;
-- id=2  hex=42
Enter fullscreen mode Exit fullscreen mode

t and #mysql50#t are two entirely distinct identifiers at the SQL level — they don't even share a column definition (BINARY(10) versus BINARY(1)). On a correctly-behaving server, these should be two completely independent tables. They aren't.

SELECT id, HEX(c1) FROM t ORDER BY id;

id=1  hex=41414141414141414141
id=2  hex=42000000000000000000
Enter fullscreen mode Exit fullscreen mode

The id=2 row — inserted only into #mysql50#t — shows up when querying the original table t. The #mysql50# prefix influenced cache-key computation in a way that made the server treat the two as the same shared table (share) with the same handler. The one-byte value inserted (0x42) gets reinterpreted against t's BINARY(10) definition, padded with nine zero bytes — proof that two "separate tables" were, in fact, sharing a single storage slot the entire time.

Step 4 — impact: what's public stops at "silent corruption"; the rest is undisclosed

MariaDB's official advisory describes the impact broadly, as "file access, crashes, data corruption, data exfiltration, or remote code execution." What the public PoC actually demonstrates is limited to data corruption (which succeeds silently, with no error message at all) and the fact that unencoded files accumulate in the data directory. The fact alone that tables with different names can bleed into each other at the cache level is already a serious problem for any application built on the assumption of privilege separation or data integrity. The specific chain that leads to crashes, exfiltration, or remote code execution isn't documented in detail in any public source.

Full PoC walkthrough — the actual published reproduction steps

Researcher SeanDishman published a PoC for this CVE bundled together with CVE-2026-85746 and CVE-2026-85748 in a single repository. Here's the full structure and what each part demonstrates.

Lab environment setup

# server/docker-compose.yml
services:
  mariadb:
    image: mariadb:13.0.1-rc
    environment:
      MARIADB_ROOT_PASSWORD: root
      MARIADB_DATABASE: appdb
      MARIADB_USER: lowpriv
      MARIADB_PASSWORD: lowpriv
    ports:
      - "3306:3306"
    command: ["--core-file"]
Enter fullscreen mode Exit fullscreen mode

One design choice here is worth flagging. The lowpriv account gets only ALL on the appdb schema — no FILE, no SUPER, no global privilege, no plugin-loading capability whatsoever. In other words, this PoC is deliberately built to demonstrate that it works with nothing more than an ordinary account that creates and writes tables within its own schema — the kind any typical application might have. The --core-file flag exists for the other two CVEs (which crash the server); this CVE on its own doesn't bring the server down.

Running it

cd server && bash start.sh
pip install -r requirements.txt
python client/exploit.py --cve 92262
Enter fullscreen mode Exit fullscreen mode

client/exploit.py does nothing more exotic than send the same SQL statements found in client/sql/cve-2026-92262.sql, one at a time, over the standard MariaDB protocol. There's no exploit framework, no shellcode involved — it's just an automated version of what any ordinary SQL client would do.

The core payload — five lines proving table-cache confusion

CREATE TABLE t (id INT, c1 BINARY(10)) ENGINE=MEMORY;
INSERT INTO t VALUES (1, REPEAT(0x41, 10));
CREATE TABLE `#mysql50#t` (id INT, c1 BINARY(1)) ENGINE=MEMORY;
INSERT INTO `#mysql50#t` VALUES (2, 0x42);
SELECT id, HEX(c1) FROM t ORDER BY id;
Enter fullscreen mode Exit fullscreen mode
  • Lines 1–2: create an ordinary table t and insert one row with the value "0x41 repeated ten times." This becomes the baseline for checking later whether t's data stayed unchanged.
  • Line 3: create "another table" — with a different column definition entirely — carrying the #mysql50# prefix. On a correct server, this should be a third table, entirely unrelated to t.
  • Line 4: insert a completely different row into that "other table."
  • Line 5: query the original t again. If there's no cache confusion, this should return only the one row inserted in line 1.

What was observed, and what it means

BEFORE   id=1  hex=41414141414141414141
MYSQL50  id=1  hex=41
MYSQL50  id=2  hex=42
AFTER_T  id=1  hex=41414141414141414141
AFTER_T  id=2  hex=42000000000000000000
Enter fullscreen mode Exit fullscreen mode

In the AFTER_T section — querying the original table t again — the id=2 row, which was only ever inserted into #mysql50#t, shows up mixed in. This is the entire point the PoC proves: two syntactically distinct CREATE TABLE statements executed, yet internally the server treated them as one object sharing a single storage slot.

A second payload demonstrates the missing encoding on its own.

CREATE TABLE `#mysql50#a-b` (id INT) ENGINE=MyISAM;
Enter fullscreen mode Exit fullscreen mode

Checking the data directory after this command shows unencoded files named a-b.frm, a-b.MYD, a-b.MYI — and a@002db.frm, what correct encoding should have produced, simply doesn't exist. The PoC repository states that these two payloads alone are sufficient to establish "data corruption, and writing unexpected filenames under the data directory" — and it does not publish a chain extending this further into a crash or code execution.

What this PoC tells you — and what it doesn't

This PoC is focused on demonstrating a flaw that reproduces quietly. The server doesn't crash, and nothing shows up as a warning in the error log — which arguably makes it more dangerous, since an operator has no obvious way to notice an attack succeeded. What this PoC does not show is exactly how this cache confusion or unencoded-file creation gets escalated into a crash or arbitrary code execution. The "remote code execution" possibility named in the vendor advisory may require these two primitives to be combined with some additional condition — for instance, a crafted filename colliding with a different path the server later references — but that specific path can't be confirmed from publicly available material alone.

The patch — separating "skip" from "confuse"

The patched versions keep the #mysql50# prefix's compatibility purpose intact (referencing a legacy-encoded name without re-encoding it), while fixing it so the prefix no longer affects table cache key computation. #mysql50#t and t are no longer treated as the same cache entry.

The lesson here is clear. When a compatibility "exception branch" doesn't precisely control the scope of what it affects, it can end up influencing far more than intended (skipping filename encoding) — reaching into things like cache identity and object-equality decisions. When two different subsystems (filename generation and cache-key computation) share the same string preprocessing logic, changing that one piece of logic can quietly change behavior in both places at once.

Remediation

  • Update MariaDB to the patched version for your branch: 10.6.28, 10.11.19, 11.4.13, 11.8.9, 12.3.3, or 13.0.2.
  • Until you update, if your application has no need for CREATE TABLE/RENAME TABLE statements using a #mysql50#-prefixed name, filtering out that query pattern at the application or proxy layer can serve as a temporary mitigation.
  • Because this flaw requires no special privilege (an ordinary CREATE/INSERT grant on your own schema is enough), it deserves particular priority in multi-tenant environments or SaaS-style services where users can create their own tables.
  • If exposure is possible, check the data directory for unencoded filenames containing special characters (.frm/.MYD/.MYI files with anything beyond alphanumerics and underscores), and review query logs for table creation or manipulation attempts using the #mysql50# prefix.
  • Given this flaw's tendency to succeed silently, don't fully trust data integrity from before the patch was applied — for critical tables, it's worth running an integrity check against a known-good backup rather than assuming nothing happened.

Top comments (0)