Skip to main content

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.

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.