SEC.PRIV.GRANT_SCOPE_BROAD_IN_MIGRATION — A grant whose scope is a wildcard
- Category: security
- Severity: medium
- Level: 0 — reachable at any strictness; the security suite is gated by
security.min_severity - Confidence: deterministic
- Stability: stable
- Suites:
lint,security - Applies to: MySQL
What it reports
A MySQL GRANT whose ON clause names a wildcard scope:
ON *.*— every database on the server.ON db.*— every table in that database.
Backticked spellings (ON `app`.`*`) are the same statement and are reported too.
Why
MySQL's ON clause names a scope, not an object, and two of its shapes keep widening on their
own. ON app.* covers the table somebody adds next month, and nothing about adding that table
mentions the grant. A privilege set that grows without anybody editing it is one nobody reviews
again.
ON *.* is the sharper of the two by a distance: an account with a server-wide grant is not scoped
to the application at all, so a compromise of it is a compromise of everything the server hosts —
including databases that did not exist when the credential was issued.
What to do
Reported:
DB::statement("GRANT SELECT ON app.* TO 'app_runtime'@'localhost'");
Not reported:
DB::statement("GRANT SELECT ON app.orders TO 'app_runtime'@'localhost'");
Name the tables the account uses. If the account genuinely follows the schema — a reporting reader over a warehouse database — accept the id with a reason rather than narrowing the rule.
What it does not see
- It cannot tell a deliberate database-wide grant from an accidental one. A read-only analytics
account over a reporting database is a legitimate use of
ON db.*. - It reads the statement, not the server: a scope this migration narrows again later is still reported, and privileges the account already held are not.
Not reported on PostgreSQL
PostgreSQL has no db.* and no *.*. Its nearest construct, GRANT … ON ALL TABLES IN SCHEMA …,
is a snapshot — it does not reach tables created afterwards, and ALTER DEFAULT PRIVILEGES is
the separate, explicit statement for the forward-looking case. That is a genuinely different
guarantee, so the question this rule asks does not exist there.
Related
SEC.PRIV.GRANT_ADMIN_IN_MIGRATION— the same statement asked about the PRIVILEGE rather than the scope. An administrative privilege can only be grantedON *.*, so a statement carrying one trips both, and that is MySQL's grammar rather than a duplicate.SEC.PRIV.GRANT_WILDCARD_HOST_IN_MIGRATION— the third axis: where the account may connect from.
Suppressing it
// config/sqlens.php
'ignore' => [
['rule' => 'SEC.PRIV.GRANT_SCOPE_BROAD_IN_MIGRATION', 'reason' => 'why this project accepts it'],
],
A suppression needs a reason, and the reason is read by whoever inherits the project.