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 withERROR 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:
- A user specifies a table name, as in
CREATE TABLE t (...). - The server encodes it for the filename character set via something like
tablename_to_filename(). Characters like-and@become@002d,@0040, and so on. - 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;
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;
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
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
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
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"]
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
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;
- Lines 1–2: create an ordinary table
tand insert one row with the value "0x41 repeated ten times." This becomes the baseline for checking later whethert'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 tot. - Line 4: insert a completely different row into that "other table."
- Line 5: query the original
tagain. 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
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;
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 TABLEstatements 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/INSERTgrant 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/.MYIfiles 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)