phpMyAdmin 5.2.3, ImportLdi TERMINATED BY SQLi
phpMyAdmin 5.2.3 ImportLdi concatenates POST ldi_terminated and ldi_new_line into LOAD DATA TERMINATED BY. Enclosed and escaped go through escapeString. A quote in ldi_terminated is SQL on the user connection. Labbed. Not LFI. No CVE yet.
- Name
- phpMyAdmin 5.2.3, ImportLdi TERMINATED BY SQLi
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- medium
- Disclosure Status
- public
- Vendor
- phpMyAdmin
- Affected
- phpMyAdmin 5.2.3 tag RELEASE_5_2_3 (962857e). Authenticated cookie login. Table import, format ldi. FILE-gated follow-on is not granted in the lab and is not claimed.
- Published
- 12 Oct 2026
- Updated
- 12 Oct 2026
- Tags
- 0-day, phpmyadmin, sqli, import, cwe-89, authenticated
Two delimiters were escaped. Two were not
I am @abraxas_null. The proof of concept is on GitHub: abraxas/phpmyadmin-importldi (loopback client). The lab stack is lab/: docker-compose.yml, init.sql, run.sh, poc.py. Authorized lab only. It talks to loopback.
This is phpMyAdmin 5.2.3 (962857e4f63d42e38f11ff4d63f5e722018add76). No CVE yet. CWE-89. 6.5 Medium (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N). Remote SQLi. Authenticated cookie login. Table import, format ldi.
phpMyAdmin sets PMA_ENABLE_LDI on /import so MYSQLI_OPT_LOCAL_INFILE is on. Filesystem follow-on needs MySQL FILE / local_infile. This lab does not have that grant and does not claim LFI. The leftover is the unescaped delimiter inside a statement PMA builds for the signed-in user. That user already has a SQL box. The interesting part is that PMA concatenates POST into LOAD DATA and only quotes half of the fields.
Why phpMyAdmin
I had already published the CustomServer WebAuthn leftover on this pin. Import plugins that build LOAD DATA by hand are a quoting audit. CSV, SQL, and XML import have had PMASAs. LDI is the LOAD DATA INFILE wrapper. Four delimiter options. Two call escapeString. Two concatenate.
I only wanted leftovers that survived 5.2.2. Claiming LFI without FILE is how you inflate a Medium SQLi into a file bug you did not prove. The lab stops at the broken TERMINATED BY literal.
Pinning RELEASE_5_2_3
Latest 5.2.x GA. Tag RELEASE_5_2_3. Commit 962857e4f63d42e38f11ff4d63f5e722018add76. Official image phpmyadmin:5.2.3. mysql:8.0 with local_infile=1 so LDI import actually runs. The account has no FILE grant. Pin source on the image still has '.$ldi_terminated.' next to $dbi->escapeString($ldi_enclosed).
The skip list is half the hunt
- PMASA-2025-1/2/3. Already in 5.2.2.
- Older LOAD DATA / import CVEs were often file-path or format-plugin issues. This is delimiter quoting inside the SQL PMA builds.
- Control-user / pmadb SQLi is a different leftover on this pin (central columns, designer JSON). ImportLdi runs on the user connection.
Closed on this lab: unauthenticated access, control-user, OS include LFI, FILE-gated read/write.
Other 5.2.3 leftovers from the same review sit in their own packs. This post is TERMINATED BY.
Surfaces that returned nothing
I read ldi_enclosed and ldi_escaped first because those are the fields that look injectable. They are escaped. Negative control. Then ldi_terminated and ldi_new_line. Concat. Then skip_queries, which should be an int-cast and is not the leftover I labbed.
The SQL box is not the finding. A signed-in user can already type LOAD DATA. The leftover is PMA interpolating a quote into a statement it builds for a UI that looks like a delimiter field.
The leftover
$sql .= ' FIELDS TERMINATED BY \'' . $ldi_terminated . '\'';
...
$sql .= ' LINES TERMINATED BY \'' . $ldi_new_line . '\'';ldi_enclosed / ldi_escaped call $dbi->escapeString. Common.php sets PMA_ENABLE_LDI on /import + format=ldi. Routes GET|POST /import and GET|POST /table/import.
A signed-in user POSTs format=ldi and a quote in ldi_terminated. The issued LOAD DATA contains the raw POST inside TERMINATED BY.
Wrong turns in the order they happened
I almost claimed LFI because PMA_ENABLE_LDI turns MYSQLI_OPT_LOCAL_INFILE on. FILE was not granted. I did not claim it.
I treated the SQL box as the finding. The leftover is the unescaped delimiter PMA concatenates.
I considered control-user / pmadb. ImportLdi is the user connection.
Theatre: a reverse shell. The oracle is TERMINATED BY '',PMA-IMPORTLDI-WITNESS' next to an escaped ENCLOSED BY.
The lab
phpmyadmin:5.2.3 + mysql:8.0 on loopback. Host bind 127.0.0.1:19290. Compose project phpmyadmin-importldi. Table appdb.ldi_target. Cookie login. POST /import with format=ldi and a quote in ldi_terminated.
Issued SQL: LOAD DATA LOCAL INFILE ... INTO TABLE \ldi_target` FIELDS TERMINATED BY '',PMA-IMPORTLDI-WITNESS'. The same quote in ldi_enclosedstored as an escapedENCLOSED BY`.
python3 phpmyadmin-importldi-Abraxas-Labs.py
# or: cd lab && ./run.shLast line:
SUCCESS phpmyadmin-importldi pin=RELEASE_5_2_3 how=live TERMINATED-BY-raw ENCLOSED-escaped PMA-IMPORTLDI-WITNESSWhat an attacker can do
A signed-in phpMyAdmin user who can open Table import can inject SQL into LOAD DATA ... TERMINATED BY on their MySQL connection.
| Privilege | Method / sink | Recon / integrity / availability | Lab-proven or inferred |
|---|---|---|---|
Break the FIELDS TERMINATED BY literal |
POST index.php?route=/import format=ldi ldi_terminated with a quote |
Integrity of the statement PMA builds | Lab-proven. HTML SQL box: TERMINATED BY '',PMA-IMPORTLDI-WITNESS' |
Keep ENCLOSED BY in-band |
Same POST, quote in ldi_enclosed |
Negative control | Lab-proven. ENCLOSED BY is escaped |
| Extra SQL on the user connection (INTO OUTFILE / stacked if allowed) | Same sink once the string is closed | Integrity of the user connection | Inferred. Lab stops at the broken delimiter. Default mysqli::query is single-statement |
| MySQL-side file read/write | Same LOAD DATA if the account has FILE or LOCAL INFILE to the web server |
Integrity of files mysqld can touch | Inferred. Not granted here. Not claimed |
Closed on this lab: unauthenticated access, control-user / pmadb, OS include LFI, and FILE-gated read/write.
The fix
escapeString on ldi_terminated and ldi_new_line, the same as enclosed and escaped. Int-cast skip_queries.
Contact
Abraxas Labs - abraxaslabs.tech - github.com/abraxas - @abraxas_null - abraxas.null@proton.me