Skip to content

[BUG] No per-user goals limit allows unbounded database row insertion (storage DoS) #698

@MehtabSandhu11

Description

@MehtabSandhu11

Summary

The POST /api/goals endpoint creates a new goal row in Supabase for every request, with no cap on how many goals a single user can have. An authenticated user (or a script with a valid session) can create millions of rows in the goals table, causing storage exhaustion and degraded query performance for all users.

Root Cause

In src/app/api/goals/route.ts, the POST handler validates input fields (title length, target range, recurrence) but never checks how many goals the user already has:

export async function POST(req: Request) {
  // ... validation ...
  const { data: goal, error } = await supabaseAdmin
    .from("goals")
    .insert({ user_id: user.id, ... })  // ← no pre-check on existing count
    .select()
    .single();

There is no MAX_GOALS_PER_USER guard anywhere in the route. The GET handler also fetches all goals with no LIMIT clause beyond Supabase's default (which is large):

const { data: goals } = await supabaseAdmin
  .from("goals")
  .select("*")
  .eq("user_id", user.id)
  .order("created_at", { ascending: false });
// no .limit() here

A single authenticated user can:

  1. Script rapid POST requests to create thousands of goals
  2. Cause the GET handler to return an enormous payload on every dashboard load
  3. Trigger the Promise.all(goals.map(...)) period-reset logic in GET to fire thousands of concurrent Supabase queries per request

Impact

  • Storage exhaustion: Supabase free tier has a 500MB database limit; unbounded inserts can fill it for all users of the instance
  • Performance: The GET handler runs Promise.all over every goal to check/reset periods — with thousands of goals this becomes extremely slow and can time out
  • Availability: Other users' dashboard loads slow down or fail when the goals table grows very large, since goals_user_period index scans still degrade at scale

Expected Behaviour

There should be a hard cap on the number of goals per user (a reasonable default: 20–50). The POST endpoint should return 429 Too Many Requests or 400 Bad Request with a clear message when the limit is reached.

Proposed Fix

Add a count check before the insert in src/app/api/goals/route.ts:

const MAX_GOALS_PER_USER = 20;

const { count } = await supabaseAdmin
  .from("goals")
  .select("*", { count: "exact", head: true })
  .eq("user_id", user.id);

if ((count ?? 0) >= MAX_GOALS_PER_USER) {
  return Response.json(
    { error: `You can have at most ${MAX_GOALS_PER_USER} goals` },
    { status: 400 }
  );
}

Also add .limit(MAX_GOALS_PER_USER) to the GET query as a safety net.

Labels

bug advanced security ~2h

Please this issue to me under GSSoC

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions