Skip to content

product_votes INSERT is WITH CHECK (true), so votes can be cast under another user_id #236

Description

@cekuu35

Disclosure first: I submitted a tool to DevHunt today, so I have a stake in the leaderboard being trustworthy. That is also why I went looking at supabase/dump.sql rather than just launching.

product_votes INSERT does not check who the vote belongs to.

CREATE POLICY "Enable insert for authenticated users only" ON public.product_votes
  FOR INSERT TO authenticated WITH CHECK (true);

CREATE POLICY "Enable delete for users based on user_id" ON public.product_votes
  FOR DELETE TO authenticated USING ((auth.uid() = user_id));

The DELETE policy states the rule: a vote belongs to user_id. The INSERT policy does not enforce it, so an authenticated caller can insert a row with any user_id, not only their own.

The primary key (user_id, product_id) does real work here — it stops the same pair being inserted twice, so this is not simple ballot stuffing on one account. What it does not stop is casting votes as other accounts. Profile ids are readable (profiles has a public SELECT policy), so the ids needed are not a secret.

For a site whose homepage is a weekly ranking with a countdown, that is the one integrity property that has to hold, and right now it rests on nobody trying rather than on the database.

The fix matches the DELETE policy you already wrote:

DROP POLICY "Enable insert for authenticated users only" ON public.product_votes;

CREATE POLICY "Enable insert for own votes only" ON public.product_votes
  FOR INSERT TO authenticated WITH CHECK ((SELECT auth.uid()) = user_id);

Same shape on three other tables

-- products: any owner_id can be inserted
CREATE POLICY "Enable insert for authenticated users only" ON public.products
  FOR INSERT TO authenticated WITH CHECK (true);
-- while UPDATE is correctly scoped:
CREATE POLICY "Enable update only for owners" ON public.products
  FOR UPDATE TO authenticated USING ((auth.uid() = owner_id)) WITH CHECK ((auth.uid() = owner_id));

So a submission can be attributed to another user at insert time, even though that user could then edit it and the inserter could not. comment and comment_vote have the same WITH CHECK (true) on INSERT, which allows comments posted under another account's id.

In each case the corresponding USING clause elsewhere on the table already expresses the intended ownership rule — it just is not applied to the write side. That asymmetry is easy to miss because USING and WITH CHECK read as the same idea and only one of them governs inserts.

Worth confirming against production rather than the dump, since this file may be behind:

select c.relname, p.polname, p.polcmd, pg_get_expr(p.polwithcheck, p.polrelid) as with_check
from pg_policy p join pg_class c on c.oid = p.polrelid
where c.relname in ('product_votes','products','comment','comment_vote') and p.polcmd = 'a';

Any row where with_check is true is one of the above.

One unrelated note while here: "Enable update for users based on email" on products is granted TO service_role with USING (true). Harmless, since service_role bypasses RLS anyway — but that means the policy is doing nothing, and its name suggests it was meant to do something else. Might be worth removing so it does not read as an active control.

Nothing here involved touching the site; it is all in the public dump.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions