
phpMyAdmin 5.2.3 - High - Remote - XSS - Designer Visual Query Builder
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · phpmyadmin-vqb-xss
Class: Stored XSS Reach: Remote
phpMyAdmin 5.2.3 (RELEASE_5_2_3 / 962857e) - phpMyAdmin
js/src/designer/history.js builds visual-query history with innerHTML. Column names go through $('<div/>').text(...).html(). Table names do not. TableName still allows < and >. A low-privilege account with CREATE TABLE on a shared database plants an HTML identifier. When a higher-privilege user opens Designer, getTableName() is sitting on the page and DesignerHistory.display() writes it into #ab.
A stored XSS in the phpMyAdmin origin. Same class as PMASA-2025-1, different feature. Check-tables headings were escaped. This path was not.
| ID | no CVE yet |
| Class | Stored XSS |
| Reach | Remote (authenticated CREATE TABLE; victim opens Designer) |
| CWE | CWE-79 |
| CVSS | High: 8.0 CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N |
| Product | phpMyAdmin |
| Affected | 5.2.3 tag RELEASE_5_2_3 (962857e4f63d42e38f11ff4d63f5e722018add76) |
| Auth | authenticated. Attacker needs CREATE TABLE on a database the victim opens in Designer |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 only |
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. .small_tab_pref_click_opt pre-escapes table_name and Twig autoescapes it again, so that click currently feeds entities into getTab(). That is not a fix of history.js. Column cells stay text-escaped (c<b id=pma-col>y, no <b id="pma-col"> node).
Same pin: WebAuthn CustomServer leftover.
I pinned phpMyAdmin RELEASE_5_2_3. PMASA-2025-1/2/3 are already in 5.2.2. Designer still builds VQB history HTML by hand.
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>';
libraries/classes/Dbal/TableName.php only checks non-empty, max 64, no trailing space. < and > are legal. MySQL accepts them in a quoted identifier.
templates/database/designer/database_tables.twig copies designerTable.getTableName() into table_name on .small_tab_pref_1. The VQB option cell uses a pre-escaped copy of the same name.
Wrong turns already recorded: treating the double-escaped option-button attribute as the fix (it is not - small_tab_pref_1 still exposes the raw name, and history.js is the sink); treating the column helper as covering table names; claiming unauthenticated XSS; a reverse shell. Theatre.
python3 phpmyadmin-vqb-xss-Abraxas-Labs.py
# or: cd lab && ./run.sh
Target only http://127.0.0.1:19250. Images phpmyadmin:5.2.3 and mysql:8.0. Compose project phpmyadmin-vqb-xss. Playwright Chromium in the poc service. Stack tears down after the run.
Two users, shared shareddb. 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.
SUCCESS phpmyadmin-vqb-xss h3-markup=yes col-escaped=yes how=live PMA-VQB-XSS-WITNESS
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.
962857e4f63d42e38f11ff4d63f5e722018add76)js/src/designer/history.jslibraries/classes/Dbal/TableName.phpGNU Affero GPL v3.0. See LICENSE. Loopback lab only. No warranty.