Skip to content

feat(safegres): L9/L10 — write reachability through definer views and rewrite rules - #1612

Merged
pyramation merged 1 commit into
mainfrom
feat/view-write-reach
Aug 2, 2026
Merged

feat(safegres): L9/L10 — write reachability through definer views and rewrite rules#1612
pyramation merged 1 commit into
mainfrom
feat/view-write-reach

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

L8 models what a view lets an untrusted role read. This adds the two write paths that escape it, both verified against PostgreSQL 18 in a scratch schema rather than inferred from the docs:

  • L9 — auto-updatable DEFINER view. A simple view over one relation is updatable, so Postgres rewrites INSERT/UPDATE/DELETE on the view onto the base relation; on a non-security_invoker view that rewritten command is permission-checked against the owner. anon holding nothing but INSERT on the view writes a table it has no ACL row on. The view body says which relation; only pg_relation_is_updatable(oid, true) says the write lands there at all.
  • L10 — rewrite rules. Any rule other than the view's own _RETURN rule is invisible to pg_get_viewdef, so ON INSERT TO v DO INSTEAD INSERT INTO audit reaches a relation the definition never names. The probe result that matters: security_invoker does not govern rule actions — an invoker view with such a rule still writes audit as the view owner. L10 therefore fires on invoker views too, which L9 deliberately does not.

Both are info / score-neutral, same posture as L7/L8. Considered severity once proven: L9 is high (an anonymous write to a table no grant names it on, under an owner that is frequently RLS-exempt — strictly worse than the L8 read it mirrors) and L10 medium (same escalation, but rules are rare and overwhelmingly DO INSTEAD NOTHING). They ship info because they are new and the write path has more ways to be wrong than the read one, not because the finding is informational.

Catalog

introspectViews now also returns, per view:

writable: Array<'INSERT' | 'UPDATE' | 'DELETE'>;  // pg_relation_is_updatable bits 8/4/16
insteadOfTriggers: boolean;                        // pg_trigger.tgtype & 64, non-internal
rules: Array<{ name; event; instead; definition }>; // pg_rewrite, rulename <> '_RETURN'

Model

One new edge on RoleReachEdge, and the write projection alongside computeViewReach:

| { kind: 'rule'; view: string; rule: string; owner: string }

computeViewWriteReach(views: ViewWriteInput[], graph, roles): RoleReach[]
// cell.privileges = [what the write does to the target]
// edge.via        = what the caller had to hold on the view to trigger it

via and privilege come apart for rules: ON DELETE ... DO INSTEAD UPDATE audit is reached with DELETE on the view and costs UPDATE on audit.

definer-view.ts exports its relation resolver (buildRelationIndex / resolveRelation) so the write analysis resolves references exactly as L8 does rather than growing a second, subtly different resolver. extractAccess() is new in callgraph/extract.ts: same parse as extractQuery, but tagging each RangeVar with the command that reaches it (InsertStmt/UpdateStmt/DeleteStmt relation → that privilege, everything else → SELECT), which is what reading a rule action requires.

What it refuses to say

Every one of these suppresses rather than guesses, and none of the remedies is a revoke — the grant on the view is what the API serves:

  • INSTEAD OF triggers → the write lands wherever a function body puts it; not followed, so suppressed.
  • A body that does not resolve to exactly one relation (a join, an unpinnable name) → no write placed.
  • An unreadable rule action, or one whose target is itself a view → suppressed.
  • DO INSTEAD NOTHING → reaches nothing, reports nothing. This is the commonest rule in the wild (~120 of them in constructive-db) and the correct answer is silence.
  • The ON ... TO <view> relation in a ruledef is the trigger, not a target, and is dropped.
  • No finding when the role already holds that privilege on the target: the view launders nothing.

Corpus

Four cases, two of them negatives (27/29 fire, 28/30 must not):

case shape pins
27-definer-view-write auto-updatable definer view, INSERT to anon L9 fires
28-invoker-view-no-write same shape, security_invoker = true L9/L10 forbidden
29-rewrite-rule-bypass invoker view + DO INSTEAD INSERT INTO audit_log L10 fires, L9 forbidden
30-instead-nothing-rule read-only view, three DO INSTEAD NOTHING rules L9/L10 forbidden

Verification

pnpm build, pnpm lint, pnpm test green in packages/safegres — 368 tests, 31 suites, including 26 new unit tests and the two new introspection assertions against a live PG. Score and fingerprint tests unchanged.

Ran the full bin/safegres-check.cjs against constructive-db with this build: zero L9 and zero L10 findings, score unchanged at 100 (A+) / 91.2 (A) — the ~120 rules there are all DO INSTEAD NOTHING on read-only views and the managed views are security_invoker, so the new rules stay silent on a real schema.

Touched shared files (minimal, additive)

  • src/commands/audit.ts — one import, the L9/L10 role/enablement lines, one dispatch block.
  • src/index.ts — one export block.
  • src/config/presets.ts, src/rules/registry.ts — the two new rule entries.

Link to Devin session: https://app.devin.ai/sessions/f340c08768814b278a179aea7994f924
Requested by: @pyramation

@pyramation pyramation self-assigned this Aug 2, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@pyramation
pyramation merged commit 4d27c97 into main Aug 2, 2026
16 checks passed
@pyramation
pyramation deleted the feat/view-write-reach branch August 2, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant