RowShield
Guides

WITH CHECK (true): anyone can write anything

Read holes leak your data. Write holes hand your database over. An INSERT, UPDATE or ALL policy whose WITH CHECK is constant true accepts any row from any caller it targets — attributed however the caller likes.

The damage compounds quietly. Forged rows poison analytics, corrupt tenant boundaries in multi-tenant schemas, and become storage for spam or phishing content served from your domain.

Rules that check this

What actually happens

WITH CHECK is the clause Postgres evaluates against new rows on INSERT and against modified rows on UPDATE. Constant true means every candidate passes. Combined with a permissive TO anon policy, your writes endpoint is open to everyone holding the public key — which is everyone.

Unlike a read hole, this one can be invisible in the product for weeks: forged rows may sit among thousands of legitimate ones, detectable only as oddities nobody attributes to a policy.

The mechanics differ by command. On INSERT the clause judges the candidate row; on UPDATE it judges the modified version while USING judges the original; an ALL policy applies both clauses to both commands. A constant true therefore opens every write shape at once — inserts attributed to anyone, updates redirecting other people's rows, and combinations of the two that are harder to untangle in data later.

Finding the open path

List write policies and their check expressions; the constant-true forms stand out quickly at this size:

Sort the output so INSERT and ALL commands sit together, one command at a time: scanning a single command keeps the check expressions comparable, and any WITH CHECK reading true, 1 = 1, or arriving blank deserves immediate attention rather than a ticket.

SELECT tablename, policyname, roles, cmd, with_check
FROM pg_catalog.pg_policies
WHERE schemaname = 'public'
  AND cmd IN ('INSERT', 'UPDATE', 'ALL', 'DELETE')
ORDER BY tablename;

Closing it

Constrain writes to the caller's own identity. The SELECT policy locking reads does nothing here — read and write conditions are evaluated independently, which is precisely why the write clause must exist on its own terms:

Expect the error text to change shape afterwards. With the clause open, forged writes succeed silently; once scoped, the same request fails with a row-level-security violation naming the policy. Applications that treated success as guaranteed need to handle that failure explicitly, which is another reason to stage the change behind a deploy rather than editing a policy live.

-- Before: anyone can insert as anyone
-- CREATE POLICY "insert_any" ON public.reviews FOR INSERT TO anon WITH CHECK (true);

CREATE POLICY "members_insert_own_reviews"
  ON public.reviews FOR INSERT TO authenticated
  WITH CHECK ((SELECT auth.uid()) = reviewer_id);

Detection and follow-up

RowShield flags INSERT/UPDATE/ALL policies whose WITH CHECK is absent or constant true, reporting per table and role so one fix silences one alert. Because scans diff between runs, a write hole opened mid-week alerts as created within one cycle, and closing it marks the transition resolved. If the placeholder returns next month, the alert is a regression — treated with the urgency it deserves.

Variations you will meet

The hole wears costumes. A WITH CHECK written as 1 = 1, a comparison against a column that can never be null, or an OR arm ending in true all reduce to the same unconditional acceptance. Policies named innocuously — allow_writes, insert_rows — hide the breadth from reviewers who skim names instead of reading expressions.

Server-side writes deserve a separate glance. Functions declared SECURITY DEFINER run with their owner's rights and skip policy evaluation entirely, so a definer function callable by anon is a write path no table policy describes. If your RPC endpoints insert data, review the definer flag and its search_path pin together with the table findings on this page.

Verifying the fix from outside

Close the clause, then attempt the forgery once more with the same request shape. The expected result is a policy violation error rather than a created row, while the legitimate write from an authenticated owner still succeeds. Keep the forged request as a permanent staging test: a refactor that reopens the clause then fails your pipeline instead of poisoning tenant boundaries in production.

Frequently asked

Is DELETE also affected?
DELETE policies use USING rather than WITH CHECK, since there is no new row to validate — the clause being judged is the existing row. A constant-true DELETE policy is therefore a wipe invitation, and it falls to the tautology detector rather than this check, which watches the write-side clause specifically.
Can I scope writes to anon at all?
Sometimes legitimately: a contact form writing a name and message to a dedicated table is a common pattern, and anon writes can serve it well. Keep such writes confined to a purpose-built table with minimal columns, validate lengths server-side, and never extend the arrangement to tables holding user identities.
Does RowShield test writes against production?
No. Findings come from catalog metadata — the policy expressions themselves. The reachability probe issues GET, HEAD and OPTIONS requests only and refuses everything else by construction, so detection never depends on attempting a real write against your database.

Check your project in about ten seconds

Paste a URL. No signup, no writes, nothing stored.

Run the free audit
supabase with check truesupabase insert policy allow allopen write policy supabaseanyone can write to my supabase table