Skip to main content

SEC.PRIV.GRANT_PUBLIC — A privilege granted to PUBLIC

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

PUBLIC is not a group somebody joins

It is the implicit membership of every role in the database. Every role that exists holds what PUBLIC holds, and so does the role created next month for a contractor, the one an extension installs, and the one somebody adds during an incident at 3am.

That has two consequences a permission granted to a named role does not have:

It cannot be reviewed by looking at who has access. The answer is "everyone", now and in the future, so the usual audit — list the roles, check what each may do — never surfaces it.

It appears in no role listing. \du shows roles; a grant to PUBLIC belongs to none of them. It lives on the object, and the only way to find it is to look at every object.

What SQLens reports, and what it deliberately does not

Every PostgreSQL database ships with grants to PUBLIC. CONNECT and TEMPORARY on the database itself, EXECUTE on more than a hundred system functions, and — on servers older than 15 — CREATE on the public schema. None of them is anybody's decision, and a check that reported them would fire on a database created thirty seconds ago.

So this rule reports only the grants somebody in your project made. It reads the difference from pg_init_privs, which is PostgreSQL's own record of the ACL an object carried when initdb finished, or when the extension that owns it was installed. Nothing is hard-coded and nothing needs updating per version: whatever your server shipped is exactly what its own catalog says it shipped.

That also covers extensions. PostGIS grants EXECUTE on hundreds of functions; every one of them is marked as the extension's own and none of them is reported.

A function you created is a separate case worth knowing about: PostgreSQL grants EXECUTE to PUBLIC on every new function implicitly, and an implicit default leaves the ACL empty. SQLens reports the GRANT you wrote, not the default you inherited.

What the finding does not claim

That the grant is wrong. USAGE on a schema is how many applications are laid out, and EXECUTE to PUBLIC on a helper function is often exactly what was meant. The finding states that a privilege reaches every role and names which privilege — the decision stays with you, which is why it is medium and not high.

Bad

-- Every role in the database can now read this table, including roles nobody has created yet.
GRANT SELECT ON invoices TO PUBLIC;

-- And this one lets every role create objects in the schema.
GRANT USAGE, CREATE ON SCHEMA reporting TO PUBLIC;

Good

-- Name the role that needs it. A grant you can review is a grant you can revoke.
GRANT SELECT ON invoices TO reporting_reader;

GRANT USAGE ON SCHEMA reporting TO reporting_reader;

If PUBLIC already holds something it should not:

REVOKE SELECT ON invoices FROM PUBLIC;

When it is deliberate

A lookup table every application role legitimately reads, or a helper function meant to be callable by anyone, is a real case. Record the decision rather than switching the rule off:

// config/sqlens.php
'ignore' => [
['rule' => 'SEC.PRIV.GRANT_PUBLIC', 'subject' => 'public.currencies', 'reason' => 'reference data, read by every application role'],
],

A baseline entry works too, and says the same thing with a date attached.

Level and severity

Level 0, because this is not a matter of appetite for strictness — no run excludes it. What decides whether you hear about it is the risk axis: security.min_severity. Raise it above medium and these findings stop breaking the gate while still appearing in the report.