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 ON shop.* FROM 'app'@'10.0.0.%';

The second form is the one people miss. Revoking only the grant option 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. MySQL writes it REVOKE GRANT OPTION ON …; PostgreSQL revokes it per privilege, REVOKE GRANT OPTION FOR SELECT, INSERT ON TABLE … FROM …, and each engine refuses the other's form. The finding prints the statement for the engine it read, with every name quoted.

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.