Built a side project called OneDollarChat - a global chat where every message costs $1. Posted it on Reddit, got some traction, went to bed.
Woke up to find a user with a balance of $21,474,836.47. That's INT_MAX. On Christmas morning.
What happened:
My Supabase RLS policy said "users can update their own row." Sounds safe, right?
Except "their own row" included the balance column. So they just... updated it.
-- What I had (bad) CREATE POLICY "Users can update own row" ON users FOR UPDATE USING (auth.uid() = id); -- What I needed (good) CREATE POLICY "Users can update own profile" ON users FOR UPDATE USING (auth.uid() = id) WITH CHECK ( -- o...Built a side project called OneDollarChat - a global chat where every message costs $1. Posted it on Reddit, got some traction, went to bed.
Woke up to find a user with a balance of $21,474,836.47. That's INT_MAX. On Christmas morning.
What happened:
My Supabase RLS policy said "users can update their own row." Sounds safe, right?
Except "their own row" included the balance column. So they just... updated it.
-- What I had (bad) CREATE POLICY "Users can update own row" ON users FOR UPDATE USING (auth.uid() = id); -- What I needed (good) CREATE POLICY "Users can update own profile" ON users FOR UPDATE USING (auth.uid() = id) WITH CHECK ( -- only allow updating safe columns balance = (SELECT balance FROM users WHERE id = auth.uid()) ); The same user also tried XSS:
Set their avatar_url to javascript:alert("meow"). My RLS let them update that too. The avatar rendered as "MEOW" instead of a single letter initial - that's how I caught it.
Their $21M message:
"meowww mrrp :3"
I'm keeping it. It's art now.
Lessons:
- "Users can update their own row" is not a security policy - it's a vulnerability
- Allowlist columns explicitly, don't trust row-level alone
- Your first real users will try to break everything
- Ship anyway, fix fast, learn publicly
The site is https://onedollarchat.com if you want to see the chaos.