Research/phpmyadmin-importldi
0-dayNo CVEMediumPublic

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

ImportLdi.php:

PHP
$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`.

Plain text
python3 phpmyadmin-importldi-Abraxas-Labs.py
# or: cd lab && ./run.sh

Last line:

Plain text
SUCCESS phpmyadmin-importldi pin=RELEASE_5_2_3 how=live TERMINATED-BY-raw ENCLOSED-escaped PMA-IMPORTLDI-WITNESS

What 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