Research/phpmyadmin-csrf-sql
0-dayNo CVEHighPublic

phpMyAdmin CSRF token strip misses PSR-7 body on /sql

phpMyAdmin 5.2.3 builds the PSR-7 request before the CSRF check. On mismatch, $_POST is cleared. sql_query was already copied from the snapshot. Non-AJAX POST /sql still runs. Labbed. No CVE yet.

Name
phpMyAdmin CSRF token strip misses PSR-7 body on /sql
Type
0-day analysis
CVE
n/a
CVE Risk
high
Disclosure Status
public
Vendor
phpMyAdmin
Affected
phpMyAdmin 5.2.3 (962857e). Default CookieSameSite=Strict blocks many cross-site browsers. Same-site / Lax / None / HTTP-auth remain.
Published
12 Oct 2026
Updated
12 Oct 2026
Tags
0-day, phpmyadmin, csrf, cwe-352, sql

The token strip missed the request object

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

This is phpMyAdmin 5.2.3 (962857e). No CVE yet. CWE-352. 8.1 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N). Remote CSRF. Victim phpMyAdmin session cookie (or HTTP auth). No valid CSRF token.

Common.php builds the PSR-7 request from globals before checkTokenRequestParam. On mismatch, Sanitize::removeRequestVars clears $_POST and $_GET but not that snapshot. $sql_query is then copied from $request->getParsedBodyParam('sql_query'). AJAX POSTs abort on token mismatch. Non-AJAX POSTs continue. SqlController prefers stripped $_POST['sql_query'] and otherwise keeps the global.

Default CookieSameSite=Strict in 5.2.3 blocks many cross-site browsers. Same-site, Lax, None, or HTTP-auth still hit it. This is a broken CSRF control either way.

Why CSRF after Strict

Same wave as WebAuthn. Routes/CSRF was the hunt. A CSRF token that empties $_POST and then copies sql_query from a snapshot taken before the empty is not a CSRF token. Strict SameSite is a cookie attribute. It is not a substitute for failing closed.

I only wanted Critical or High. UI:R because someone has to cause the victim browser to POST. PR:N on the vector because the attacker does not have a phpMyAdmin account. They need the victim's session cookie to be sent. Strict makes classic cross-site forms fail. Same-site, a Lax/None deploy, or HTTP-auth still send it. The lab is a same-site loopback POST. Strict still sends the cookie.

Pinning RELEASE_5_2_3

Latest 5.2 stable. Tag RELEASE_5_2_3. Commit 962857e. Image phpmyadmin:5.2.3. Cookie login as appuser. Marker row PMA-CSRF-SQL-WITNESS. Table keepme for the drop-table negative.

The skip list is half the hunt

  • PMASA-2025-1 / 2 / 3 already patched in 5.2.2.
  • Logout already fails closed on mismatch. Copy that.
  • AJAX POSTs already abort on token mismatch. That is the control that /sql non-AJAX missed.
  • Handlers that only read stripped $_POST (drop-table) hold. That is the negative, not a reason to skip /sql.
  • Default Strict SameSite is documented. It is not "CSRF is closed."

I am not filing "PMA can run SQL with a valid token." I am filing SQL without one.

Surfaces that returned nothing

Same empty surfaces as the rest of the wave. Export SaveDir, partition DROP/TRUNCATE, account lock, /config/set, pmadb create are inferred siblings: any handler that reads $request->getParsedBody() after the strip. I did not exercise those. /sql is the highest-impact instance that the lab actually ran.

The snapshot

PHP
$sql_query = '';
if ($request->isPost()) {
    $sql_query = $request->getParsedBodyParam('sql_query', '');

$request was createFromGlobals() before the token check. On mismatch, $_POST is empty. The object is not. Non-AJAX continues. SqlController uses global $sql_query if $_POST is empty. Route GET|POST /sql.

AJAX plus mismatch exits. That is why the same POST with ajax_request=1 is Token mismatch and does not INSERT.

The lab

Loopback http://127.0.0.1:19240. Compose project phpmyadmin-csrf-sql. Same-site POST without a token returned the marker row PMA-CSRF-SQL-WITNESS in the SQL results. General log showed the SELECT as appuser. The same POST with ajax_request=1 returned Token mismatch and did not INSERT. POST /database/structure/drop-table without a token left keepme in place.

Plain text
cd lab && ./run.sh

Last line:

Plain text
SUCCESS phpmyadmin-csrf-sql sql-ran=yes ajax-abort=yes drop-held=yes PMA-CSRF-SQL-WITNESS

Wrong turns, in the order I made them

Treating AJAX /sql as the leftover. It aborts Token mismatch. The leftover is non-AJAX.

Treating drop-table as the leftover. That handler only reads stripped $_POST. keepme stayed. That is the negative.

Treating default Strict SameSite as "CSRF is closed." The control is still broken. Same-site / Lax / None remain. The lab is same-site on purpose.

Claiming unauthenticated PMA. Cookie login required first. The attacker needs the victim session, not a password.

A reverse shell. Theatre. The oracle is a SELECT marker on loopback.

What an attacker can do

Anyone who can cause the victim browser to POST to phpMyAdmin without ajax_request can run SQL as that MySQL user.

Privilege Method / path Lab-proven or inferred
Run attacker SQL as the victim MySQL account (integrity + recon of that SQL box) POST index.php?route=/sql sql_query=... db=... no token, no ajax_request Lab-proven. Results body contained PMA-CSRF-SQL-WITNESS. General log showed the SELECT as appuser
Abort the same handler on AJAX Same POST with ajax_request=1 Lab-proven. Token mismatch. INSERT marker row absent. General log did not run that INSERT
Skip handlers that only read stripped $_POST POST /database/structure/drop-table without a token Lab-proven. keepme still present
Same gap on export (SaveDir), partition DROP/TRUNCATE, account lock, /config/set, pmadb create Any handler that reads the PSR-7 body after the strip Inferred. Not exercised

Closed on this lab: unauthenticated PMA, AJAX routes, drop-table without a token, OS shell. Default Strict SameSite means a classic cross-site form often will not send the cookie; the leftover is still a failed CSRF check on same-site and on Lax/None deploys.

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

The fix

On mismatch, replace the request with an empty body, or refuse all POSTs, not only AJAX. Do not copy sql_query from a stale request. Fail closed like logout.

Same pin leftovers: phpmyadmin-webauthn, phpmyadmin-pmadb-central-sqli, phpmyadmin-designer-json-sqli, phpmyadmin-setup-validate.

References