SEC.PRIV.GRANT_ALL — An account holds every privilege the engine has
- Category: security
- Severity: high
- Level: 0
- Confidence: deterministic
- Downtime class: none — the finding is about a privilege, not about a statement
- Stability: stable
- Suites: audit
- Applies to: PostgreSQL 18 · MySQL 8.4
Why this one sits under all the others
Every other finding in the security suite describes a step somebody would still have to take. This one describes the destination.
An account holding every privilege the engine has needs no escalation, because there is nothing above it to escalate to. An injection that reaches it can read every row, change the schema, and — where the grant is server-wide on MySQL — read and write files through the server process. The narrower findings are worth acting on precisely because this one is not true; when it is, they are details.
The words ALL PRIVILEGES are not what is detected
Both engines expand them at grant time. MySQL writes one Y per privilege column; PostgreSQL writes
one ACL entry per privilege. Nothing anywhere records that somebody typed ALL.
So the state is recognized by comparing what the account holds against what the engine HAS for that kind of object — and each engine is asked rather than assumed:
- PostgreSQL states its own complete set through
acldefault(), which is the ACL it gives a freshly created object.MAINTAINarrived in 17 and the next one will arrive the same way; a list kept by hand would quietly stop recognizingGRANT ALLon the day it did. - MySQL is compared against the grant-table columns the reading itself is generated from, so the question and the reading cannot disagree.
An account granted every privilege one statement at a time is therefore reported exactly like one
granted ALL PRIVILEGES, because it is in the same state.
What the finding does not say
That the account is wrong. Administrative accounts, replication accounts and migration accounts are deliberately broad. This is the list of accounts for which the breadth ought to have been a decision — its value is a name on it that nobody expected.
That the application is safe when it is clean. The rule reads privileges, not behavior. A clean result means one specific consequence of an injection is off the table, not that there is no injection.
Grants the engine ships are never reported. MySQL's mysql.session holds most of the
administrative vocabulary out of the box; a rule reporting it would open on every server with a
finding nobody caused and nobody may revoke.
What it looks like
-- Everything the engine has, on everything the server holds.
GRANT ALL PRIVILEGES ON *.* TO 'app'@'10.0.0.%';
-- The same state, one database wide. Still every privilege MySQL has there.
GRANT ALL PRIVILEGES ON shop.* TO 'app'@'10.0.0.%';
What to do about it
-- What the application actually does at run time, and nothing else.
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'app'@'10.0.0.%';
-- Schema changes belong to the account that deploys them.
GRANT ALL PRIVILEGES ON shop.* TO 'deploy'@'10.0.0.%';
On PostgreSQL the same separation is spelled that engine's way — narrow the application role to the statements it runs, and leave the broad grant with the role that deploys migrations.
Splitting the runtime account from the deploying one is the same measure
SEC.PRIV.RUNTIME_DDL is about, seen from the other end. An account that also
carries WITH GRANT OPTION is reported by SEC.PRIV.GRANT_OPTION as well —
that combination lets it create a second account holding the same access, one that carries no record
of where it came from.