phpMyAdmin 5.2.3, Designer VQB history XSS
phpMyAdmin 5.2.3 Designer visual-query history concatenates table names into an h3 with innerHTML. Column names in the same block are escaped. TableName still allows < and >. A CREATE TABLE plant becomes markup in the victim origin. Labbed. No CVE yet.
- Name
- phpMyAdmin 5.2.3, Designer VQB history XSS
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- high
- Disclosure Status
- public
- Vendor
- phpMyAdmin
- Affected
- phpMyAdmin 5.2.3 tag RELEASE_5_2_3 (962857e). Authenticated CREATE TABLE on a database the victim opens in Designer. PMASA-2025-1 did not touch this path.
- Published
- 12 Oct 2026
- Updated
- 12 Oct 2026
- Tags
- 0-day, phpmyadmin, xss, designer, cwe-79, authenticated
The column helper never met the table name
I am @abraxas_null. The proof of concept is on GitHub: abraxas/phpmyadmin-vqb-xss (loopback client). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py. Authorized lab only. It talks to loopback.
This is phpMyAdmin 5.2.3 (962857e4f63d42e38f11ff4d63f5e722018add76). No CVE yet. CWE-79. 8.0 High (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N). Remote stored XSS. Authenticated CREATE TABLE. The victim opens Designer.
PMASA-2025-1 escaped Check-tables index-problem headings. It did not touch Designer visual-query history. Column names in that history already go through $('<div/>').text(...).html(). Table names are concatenated into an <h3> and assigned with innerHTML. TableName still allows < and >. MySQL accepts them in a quoted identifier. The leftover is that mismatch, not a new class of unauthenticated RCE.
Why phpMyAdmin
I had already published the CustomServer WebAuthn leftover on this pin. The rest of the 5.2.3 tree was still sitting there: Designer, import plugins, privilege oracles, preference writers, pmadb, setup scripts that outlive config.inc.php. phpMyAdmin is a large PHP UI over MySQL with a long PMASA history. Cookie auth. A control user. JavaScript that still builds HTML by hand in 2026.
I only wanted leftovers that survived 5.2.2. Local is in-scope. Medium stays unless it chains. Unauthenticated RCE and OS LPE stayed empty on this pin.
Pinning RELEASE_5_2_3
Latest 5.2.x GA. Tag RELEASE_5_2_3. Commit 962857e4f63d42e38f11ff4d63f5e722018add76. Version::VERSION = 5.2.3. Official image phpmyadmin:5.2.3. That is the pin.
6.x / master already quotes some pmadb blobs that 5.2.3 still concatenates. This leftover is not one of those. history.js is still the hand-built string on 5.2.3.
The skip list is half the hunt
Before you look for leftovers, you write down what is already public so you do not rediscover a PMASA with a press release.
- PMASA-2025-1. XSS in Check-tables index-problem headings. Escaped in 5.2.2. Not Designer VQB history.
- PMASA-2025-2. XSS on insert. 5.2.2. Insert BIT /
pma_typeleftovers looked incomplete. Confidence too low to treat as a new critical. - PMASA-2025-3. iconv. 5.2.2.
Closed on this pin from the rest of the tree: classic goto LFI (allowlist is index.php only), uri / version-check HTTP (hardcoded phpmyadmin.net), external transformation proc_open (empty $allowed_programs by default). Unauthenticated RCE and OS LPE were not found.
Other 5.2.3 leftovers from the same review sit in their own packs: WebAuthn CustomServer (already public), unquoted authentication_plugin, isSuperUser() catalog SELECT, /config/set 2FA strip, ImportLdi TERMINATED BY. This post is the Designer history <h3>.
Surfaces that returned nothing
I split Designer into the pieces that look like XSS and the pieces that already learned. Relation lines, PDF schema, the table-list filter, and the VQB OK button path were the first pass. The option-button attribute is pre-escaped in Twig and then autoescaped. A click currently feeds entities into getTab(). That looked, for a minute, like the whole feature was safe.
It is not a fix of history.js. .small_tab_pref_1 still copies designerTable.getTableName() into table_name. The live script still concatenates getTab() into an <h3>. One attribute got double-encoded. The sink did not.
The leftover
js/src/designer/history.js (shipped as js/dist/designer/history.js):
temp = historyArray[i].getTab();
str += '<h3 class="tiger"><a href="#">' + temp + '</a></h3>';
// ...
'<td width="175" style="padding-left: 5px">' + $('<div/>').text(historyArray[i].getColumnName()).html() + '<td>';Someone looked at column names and reached for the jQuery text helper. Someone looked at table names and concatenated. TableName.php only checks non-empty, max 64, no trailing space. < and > are legal.
templates/database/designer/database_tables.twig copies the raw MySQL name onto .small_tab_pref_1. Chromium does not have to invent the identifier. It is already in the DOM. DesignerHistory.display() writes it into #ab.
Wrong turns in the order they happened
I treated the double-escaped option-button attribute as the fix. It is not. The OK click currently feeds entities. The history sink is still getTab() plus innerHTML.
I treated the column helper as covering table names. Same function, two fields, one escaped. That is the leftover.
I considered claiming unauthenticated XSS. Designer is behind cookie login. The plant is CREATE TABLE on a shared database. UI:R is the higher-priv user opening Designer.
Theatre: a reverse shell. The oracle is an <i id="pma-vqb"> node in the history <h3>, and a column cell that stays text.
The lab
phpmyadmin:5.2.3 + mysql:8.0 on loopback. Two users, shared shareddb. Playwright Chromium in the poc service because the sink is a browser innerHTML. Host bind 127.0.0.1:19250. Compose project phpmyadmin-vqb-xss.
Low-priv creator runs CREATE TABLE \tx` (`cy` INT). Higher-priv victimlogs into phpMyAdmin and opens Designer withquery=1. Live history.js is the copy from the container (h3-unescaped=yes, column helper present). Chromium calls DesignerHistory.display()with the table name fromgetTableName()` on the page.
Result: #ab h3.tiger innerHTML is <a href="#">t<i id="pma-vqb">x</i></a>. Column td text is c<b id=pma-col>y with no b#pma-col node. Markup only. Not a script payload.
python3 phpmyadmin-vqb-xss-Abraxas-Labs.py
# or: cd lab && ./run.shLast line:
SUCCESS phpmyadmin-vqb-xss h3-markup=yes col-escaped=yes how=live PMA-VQB-XSS-WITNESSWhat an attacker can do
Plant an HTML table name, then wait for a higher-privilege user to open Designer on that database and render visual-query history.
| Privilege | Method / sink | Recon / integrity / availability | Lab-proven or inferred |
|---|---|---|---|
| Inject markup into the phpMyAdmin origin | DesignerHistory.display() concatenates getTab() into #ab innerHTML |
Integrity of the victim origin | Lab-proven. Live history.js on phpmyadmin:5.2.3. <h3> contained <i id="pma-vqb"> |
Break out of a designer table_name attribute |
database_tables.twig small_tab_pref_1 uses designerTable.getTableName() |
Recon of the raw MySQL name in the Designer DOM | Lab-proven readable as the raw identifier |
Read the victim session cookie and CSRF token |
Same-origin script once history renders | Recon of the victim PMA session | Inferred from stored XSS in the PMA origin. This lab uses an <i> marker, not a script |
| Run SQL, manage users, export, as the victim MySQL account | Token-backed POST /sql, /server/privileges, export |
Integrity of whatever that MySQL account can touch | Inferred. Same impact class as PMASA-2025-1 |
| Change 2FA / userconfig | POST /config/set as the victim |
Integrity of the victim preference store | Inferred. Sibling leftover, not this lab |
Closed on this leftover: unauthenticated access, OS RCE, and the VQB OK button path. Column cells stay text-escaped. This is not a PHP shell on the host. It is the session of the victim MySQL user through the phpMyAdmin origin.
The fix
Escape getTab() with $('<div/>').text(temp).html() or Functions.escapeHtml before concatenating into the <h3>. Do not rely on Twig double-encoding of one attribute. Keep the column-name helper as the model.
Contact
Abraxas Labs - abraxaslabs.tech - github.com/abraxas - @abraxas_null - abraxas.null@proton.me