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:
- Script rapid POST requests to create thousands of goals
- Cause the
GET handler to return an enormous payload on every dashboard load
- 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
Summary
The
POST /api/goalsendpoint 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 thegoalstable, causing storage exhaustion and degraded query performance for all users.Root Cause
In
src/app/api/goals/route.ts, thePOSThandler validates input fields (title length, target range, recurrence) but never checks how many goals the user already has:There is no
MAX_GOALS_PER_USERguard anywhere in the route. TheGEThandler also fetches all goals with noLIMITclause beyond Supabase's default (which is large):A single authenticated user can:
GEThandler to return an enormous payload on every dashboard loadPromise.all(goals.map(...))period-reset logic inGETto fire thousands of concurrent Supabase queries per requestImpact
GEThandler runsPromise.allover every goal to check/reset periods — with thousands of goals this becomes extremely slow and can time outgoalstable grows very large, sincegoals_user_periodindex scans still degrade at scaleExpected Behaviour
There should be a hard cap on the number of goals per user (a reasonable default: 20–50). The
POSTendpoint should return429 Too Many Requestsor400 Bad Requestwith a clear message when the limit is reached.Proposed Fix
Add a count check before the insert in
src/app/api/goals/route.ts:Also add
.limit(MAX_GOALS_PER_USER)to theGETquery as a safety net.Labels
bugadvancedsecurity~2hPlease this issue to me under GSSoC