Skip to main content

SEC.PRIV.GRANT_OPTION — The account can hand its access on

  • Category: security
  • Severity: medium
  • Level: 0
  • Confidence: deterministic
  • Downtime class: none — the finding is about a grant, not about a statement
  • Stability: stable
  • Suites: audit
  • Applies to: PostgreSQL 18 · MySQL 8.4

What it actually costs

Every other grant rule asks what an account can do. This one asks what it can give away — and that is a different kind of fact.

An account that may re-grant what it holds can create a second account holding the same access. The second one carries no note saying where it came from, so a privilege audit run tomorrow sees a plain account rather than the path that made it. Any limit you place elsewhere on the server becomes voluntary at that point.

Bad

-- The account can pass this on to anybody it likes.
GRANT SELECT, INSERT ON shop.* TO 'app'@'10.0.0.%' WITH GRANT OPTION;

Good

-- The same access, without the ability to widen the server.
GRANT SELECT, INSERT ON shop.* TO 'app'@'10.0.0.%';

-- On an account that already has it, this takes back only the handing-on:
REVOKE GRANT OPTION FOR SELECT, INSERT ON shop.* FROM 'app'@'10.0.0.%';

The second form is the one people miss. REVOKE GRANT OPTION FOR leaves the access itself intact — so this finding can be acted on without changing what the application can do, which makes it cheap even where the grant itself is correct.

Both engines, one decision

This is one of the few security facts neither engine has to itself. MySQL spells it WITH GRANT OPTION and records it in the grant tables; PostgreSQL records it in the ACL, where aclexplode().is_grantable reports it. The rule carries no engine check — a driver test here would be a test about wording.

Why medium

What is handed on is access to data, so the blast radius is bounded by what this grant already covers. Where the re-grantable privilege reshapes the schema instead, that is a different sentence at a different weight: SEC.PRIV.GRANT_OPTION_STRUCTURAL.

What is not reported

A grant the engine ships. Anything that is not a project decision — initdb's own grants, an extension's — is excluded, or the rule would fire on a database created a minute ago.

A grant that could not be read in full is undetermined. "Not grantable" and "we could not see whether it is grantable" read the same to somebody who cannot tell them apart, and only one of them is good news.