Research/phpmyadmin-designer-json-sqli
0-dayNo CVEHighPublic

phpMyAdmin designer settings JSON pmadb SQLi

phpMyAdmin 5.2.3 concatenates json_encode of designer settings into a single-quoted control-user UPDATE. A quote plus hash comments out WHERE username and overwrites another user's row. Labbed. No CVE yet.

Name
phpMyAdmin designer settings JSON pmadb SQLi
Type
0-day analysis
CVE
n/a
CVE Risk
high
Disclosure Status
public
Vendor
phpMyAdmin
Affected
phpMyAdmin 5.2.3 (962857e) with designer pma__designer_settings. 6.x quoteString on ControlUser is the patched shape. 5.2.3 is not.
Published
12 Oct 2026
Updated
12 Oct 2026
Tags
0-day, phpmyadmin, sqli, pmadb, designer, cwe-89, authenticated

json_encode is not quoteString

I am @abraxas_null. The proof of concept is on GitHub: abraxas/phpmyadmin-designer-json-sqli (loopback client). The lab stack is lab/: docker-compose.yml, run.sh, poc.py, init.sql. Authorized lab only. It talks to loopback.

This is phpMyAdmin 5.2.3 (962857e). No CVE yet. CWE-89. 8.1 High (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N). Remote SQLi. Authenticated cookie login. The sink is the control user on pma__designer_settings, not the signed-in SQL box.

Designer AJAX save_setting_value json_encodes the settings blob and concatenates it into a single-quoted UPDATE/INSERT. Username is escaped. The JSON is not. json_encode does not escape a single quote. phpMyAdmin 6.x wraps this blob in quoteString on ControlUser. 5.2.3 still raw-concats it.

Older designer CVEs (2016-6616, 2019-11768, 2019-18622) were username / database name. This leftover is the JSON settings_data column.

Why this blob

Same wave as WebAuthn and central columns. The pmadb hunt had two Highs. Central columns is extra / attribute. This one is the designer JSON. 6.x already patched this shape. 5.2.3 did not. That is a leftover, not a new class.

I only wanted Critical or High. "The user already has a SQL box" is the skip for appdb. It is not a skip for phpmyadmin.* as the control user. appuser in this lab has no GRANT on phpmyadmin.

Pinning RELEASE_5_2_3

Latest 5.2 stable. Tag RELEASE_5_2_3. Commit 962857e. Image phpmyadmin:5.2.3. Designer settings table created. Control user granted DML on phpmyadmin.*. Seed a victim row {"keep":"safe"} so a missing WHERE is visible.

The skip list is half the hunt

  • PMASA-2025-1 / 2 / 3 already patched in 5.2.2.
  • Designer username / db-name CVEs 2016-6616, 2019-11768, 2019-18622. Different column. Do not re-file those.
  • CVE-2016-5703 central columns database name. Different feature.
  • 6.x quoteString(..., ControlUser) on this blob. That is the patched shape. 5.2.3 must still be raw concat or it is not this leftover.
  • Designer VQB history stored XSS is a different leftover (table names into HTML). Not this post.

I am not filing designer-off. Feature no-ops. FAIL, not this leftover.

Surfaces that returned nothing

Same empty surfaces as the rest of the wave. The interesting negative on this sink is username: it is escaped. Inject there and you are labbing the 2016 class, which is closed. Inject in value. json_encode will put your quote inside the SQL literal.

RecentFavoriteTable::saveToDb() already quotes. This function did not copy that.

The concat

libraries/classes/Database/Designer/Common.php saveSetting:

PHP
$orig_data = json_encode($orig_data);
$save_query = 'UPDATE '
    ...
    . " SET settings_data = '" . $orig_data . "'"
    . " WHERE username = '" . $this->dbi->escapeString($username) . "'";
...
    . "', '" . json_encode($save_data) . "');";
$this->dbi->queryAsControlUser($query);

Controllers/Database/DesignerController.php POST operation=save_setting_value. Route GET|POST /database/designer. $_POST['index'] / $_POST['value'].

A quote plus # in value truncates settings_data and comments out WHERE username = 'appuser'. The UPDATE hits every row. mysqli default is single-statement, so this is not stacked queries. It is a missing WHERE. That is enough.

The lab

Loopback http://127.0.0.1:19220. Compose project phpmyadmin-designer-json-sqli. Cookie login as appuser. Clean save stored {"snap_line":"grid-ok"} for appuser and left victim at {"keep":"safe"}. The same save with PMA-DESIGNER-JSON-SQLI-WITNESS'# truncated settings_data. Victim row became the truncated witness blob. General log showed the raw quote, not quoteString. sql-error=false. MySQL accepted the truncated UPDATE.

Plain text
cd lab && ./run.sh

Last line:

Plain text
SUCCESS phpmyadmin-designer-json-sqli version=5.2.3 how=live raw-concat=yes truncated=true sql-error=false quoteString=no PMA-DESIGNER-JSON-SQLI-WITNESS

Wrong turns, in the order I made them

Injecting on username. It is escaped. That is the 2016 class, which is closed.

Expecting a #1064. The # comments out the rest. MySQL accepts the truncated UPDATE. The oracle is row corruption plus the general log, not a syntax error. Central columns was the #1064 leftover. This one is quieter.

Treating stacked queries as proven. Default mysqli::query is single-statement. The missing WHERE is the lab-proven integrity hit.

Calling 6.x this leftover. 6.x quoteStrings this blob. 5.2.3 does not.

A reverse shell. Theatre. The oracle is a truncated JSON blob on loopback.

What an attacker can do

A signed-in phpMyAdmin user who can open Designer can inject SQL as the control user on pma__designer_settings.

Privilege Method / path Lab-proven or inferred
Break the control-user UPDATE/INSERT string (integrity of pmadb SQL) POST index.php?route=/database/designer operation=save_setting_value with a quote in value Lab-proven. General log raw concat. quoteString absent
Truncate own settings_data so it is no longer JSON (integrity of own row) Same POST; # comments out the rest of the statement Lab-proven. appuser blob contains the witness and is not valid JSON. Clean save grid-ok is gone
Overwrite other users' designer rows without stacked queries (integrity of victim row) Same # comments out WHERE username = '...' Lab-proven. Seed victim {"keep":"safe"} became the truncated witness blob
Extra statements as the control user Same sink if the control mysqli link allows multi-query Inferred. Default mysqli::query is single-statement. Not enabled here
Reach other pmadb tables (bookmarks, history, pma__userconfig) Only if stacked queries or a write-style breakout is possible Inferred. Control user in this lab has only phpmyadmin.* DML

Closed on this lab: unauthenticated access, the signed-in appuser SQL box (appdb only), OS shell, and stacked queries. Feature no-ops if pmadb / designer settings is off.

I am not going to print a stacked payload you can paste at someone else's PMA. If you own the instance, run the proof of concept against loopback.

The fix

quoteString(..., ControlUser) / escapeString on the JSON blob, same as RecentFavoriteTable::saveToDb() and phpMyAdmin 6.x on this column. Do not put json_encode output inside a single-quoted SQL literal.

Same pin leftovers: phpmyadmin-webauthn, phpmyadmin-pmadb-central-sqli, phpmyadmin-setup-validate, phpmyadmin-csrf-sql.

References