Skip to content
Code Recycle

Component · for humans & their agents

Matview RLS Derived Leak Scanner

verified · first-partyactively maintained$0 during beta (was $69)

Row Level Security is enabled, your policy is correct, and a materialized view over that table hands every tenant's rows to anyone who can read it. Both fixes you would try are rejected by PostgreSQL.

by Code Recycle

Get it free — beta

Every claim on this page is refundable if it is untrue — refund policy.

Verified: 21 tests

Decides whether a view or materialized view lets readers past the base table's ROW LEVEL SECURITY policies. Pure function over catalogue facts you already have: no I/O, no database connection, no pg dependency.

Decides whether a view or materialized view lets readers past the base table's ROW LEVEL SECURITY policies. Pure function over catalogue facts you already have: no I/O, no database connection, no pg dependency.

Measured against PostgreSQL 17.10. NOT a PostgreSQL bug report.

THE MEASUREMENT. One table, RLS enabled, one tenant policy, five rows across three clinics:

    CREATE MATERIALIZED VIEW patient_records_mv AS SELECT * FROM patient_records;  -- silent
    GRANT SELECT ON patient_records_mv TO low_priv;                                -- silent
          relname       | relrowsecurity          policies on the matview: 0
    --------------------+----------------
     patient_records    | t
     patient_records_mv | f
    as low_priv, app.clinic = 'clinic_a'
      base table  ->  2 rows      <- the policy works
      matview     ->  5 rows      <- all three clinics

EVERY ROW RETURNED IS REAL. Nothing is fabricated, nothing is malformed, and no amount of inspecting the output would reveal a problem -- the values simply belong to someone else. That is what makes this different in kind from a bad number: THERE IS NO WRONG SHAPE TO NOTICE.

BOTH FIXES ARE REJECTED:

    ALTER MATERIALIZED VIEW patient_records_mv ENABLE ROW LEVEL SECURITY;
      ERROR:  ALTER action ENABLE ROW SECURITY cannot be performed on relation
      DETAIL: This operation is not supported for materialized views.
    ALTER MATERIALIZED VIEW patient_records_mv SET (security_invoker = on);
      ERROR:  unrecognized parameter "security_invoker"

That is the whole reason this exists. The problem is findable and the two obvious repairs are ACTIVELY REFUSED, so a scanner that reports the leak without saying it is unfixable sends you to spend an afternoon proving PostgreSQL means it. Every finding carries a remedy field, and for a matview that field says what actually works.

A PLAIN VIEW has the identical leak -- measured, 5 rows -- and DOES have a fix: security_invoker=on, measured back down to 2. So a plain view is reported fixableInPlace:true with the one-line ALTER. A materialized view has the leak and no fix.

FORCE ROW LEVEL SECURITY DOES NOT FIX IT. IT CHANGES WHICH WRONG ANSWER YOU GET. Measured with a NON-SUPERUSER owner, same definition, same table, three refreshes:

    RLS enabled, NOT forced (the DEFAULT)        ->  5 rows captured, every tenant
    RLS FORCED, no policy context in the session ->  0 rows captured
    RLS FORCED, app.clinic = 'clinic_a'          ->  2 rows captured, all clinic_a

No error, no warning, nothing in the output distinguishing them. The third is the nastiest because it LOOKS like the fix worked -- RLS was enforced during the refresh. But those rows are then served to EVERY reader: clinic_b and clinic_c now read clinic_a's records, and their own data is gone. Filtering the refresh does not filter the read.

The second is not a breach at all, and this package refuses to report it as one: it returns SILENTLY_EMPTY, a separate status, because a dashboard of zeroes that looks like a quiet week is a correctness failure and calling it an incident would misreport something that did not happen.

IT FAILS CLOSED. refreshVisibility accepts "unknown", which produces UNDECIDABLE rather than an assumed answer -- it is the fact that decides which of the three outcomes you have. It still reports the unfixable leak while undecided, because that one is certain regardless of who refreshes. It also REFUSES a materialized view claiming securityInvoker: PostgreSQL rejects the parameter outright, so such a matview cannot exist, and answering about it would mean reporting on a schema nobody has.

WHAT IT DOES NOT REPORT. A matview over a table with no RLS has nothing to leak past, and nothing is reported. Checked BEFORE anything about the relation itself, because an ordinary matview flagged as a finding teaches people to ignore the scanner.

WHY THE PROBE USES A NON-SUPERUSER OWNER, recorded because the mistake is easy to repeat: an earlier version measured the three refresh cases as a superuser and got 5 rows for all three, since a superuser bypasses RLS regardless of FORCE. That reading was worthless and it looked exactly like a result.

RELATED: postgres-rls-bypass-verdict answers the TABLE question -- is RLS enforced for the role your app connects as -- and its remedy is FORCE ROW LEVEL SECURITY. This package answers what that remedy does to a materialized view reading the same table. They are the two halves; neither answers the other's question.

VERIFIED: 21 tests, measured by running the suite, every mutation observed FAILING before restore.

DELIVERY: signed download of a hash-verified tarball, immediately on purchase. Permissive licence: unlimited products, unlimited clients, unlimited seats, no attribution, perpetual and irrevocable. One restriction, do not republish the source as source.

Interface

What you call, and what comes back. Types and signatures only — the implementation ships with the source.

  export function evaluateDerivedRelation(input: DerivedRelationInput): DerivedLeakVerdict;
  export type RefreshVisibility = "bypasses" | "filtered-with-context" | "filtered-no-context" | "unknown";
  export type Severity = "critical" | "warning" | "note";

01Capabilities

Does

  • + Compliance audit
  • + Data exposure boundary
  • + Row level security enforcement

Doesn’t

  • No exclusions declared

02Requirements & stack

Depends on

No declared dependencies

Credentials needed

None declared

Stack

typescript

03Community

No endorsements yet

No verified confirmations yet — be the first.

Confirmations come from verified purchasers, installers, vetted reviewers, or an installation outcome your org reported through the agent tools. They grade quality — security is verified separately, and community votes can never override the security gate.

Open an issue

Sign in to confirm — weight comes from verified usage, not vote count.

0 open · 0 answered · 0 fixed · 1 said it worked

04Trust Passport

Full passport →
–/100

0/0 automated components pass. An automated score is never a security guarantee.

✓ Verified · first-partyreviewed Sep 20, 2026 · re-verification due Dec 19, 2026
  • publisher identity Publisher status verified; 1 verification(s) on file
  • malicious pattern scan No known malicious-behavior patterns across 11 source file(s) plus listing text
  • capability contract All 0 observed capability reference(s) match the declared manifest
  • agent safety scan No injection patterns in agent-readable content
  • provenance No release signature or provenance attestation
  • behavioral sandbox Not performed in this environment — requires the production isolated runner (docs/sandbox-requirements.md). No untrusted code is ever executed on the application host.

Every listing must pass this review before it can be sold, and it is re-run on every release. Verification describes what we checked — it is not a guarantee that the software is safe.

VersionChannelReleasedNotes
1.0.0stableAug 7, 2026First public release.