Edge function trusts a user id sent in the request
The function acts on whichever user id the request names, so any user can act as any other.
- Severity
- High
- Who can exploit it
- Anyone who can call the function, often any signed-in user
- Found by
- PatchLabs Code: functions and API routes
- Lab proof
- Not lab-proven: file and line in the report
What it is
A function that uses the service role key decides for itself whose data to touch. PL-022 means it takes that decision from the request: const { userId } = await req.json(), or ?user_id= in the URL, then reads or changes that user's rows. Even when the function checks that the caller is signed in, so it isn't PL-017, any user can put someone else's id in the body.
User ids are less secret than they look. They turn up in API responses, URLs, storage paths and other people's rows, so "nobody knows the id" is not a defence.
Why AI builders produce it
The frontend already has user.id to hand, so passing it to the function is the obvious way to say "for this user". The function trusts its input, and the test call always sends the right id.
Example of vulnerable code
Simplified from the patterns we see in AI-built apps. Synthetic: not taken from any real app.
// supabase/functions/get-profile/index.ts
import { createClient } from 'npm:@supabase/supabase-js@2';
Deno.serve(async (req) => {
if (!req.headers.get('Authorization')) {
return new Response('Unauthorized', { status: 401 }); // presence is not proof
}
const { userId } = await req.json(); // chosen by the caller
const admin = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
);
const { data } = await admin.from('profiles')
.select('email, phone, credits').eq('id', userId).single();
return Response.json(data);
});
Check it yourself
# Functions and API routes that read an id from the request
grep -rnE "(userId|user_id|uid|profileId)" supabase/functions src/app app/api pages/api 2>/dev/null \
| grep -E "req(uest)?\.json|searchParams|req\.body|req\.query"
Then run the function locally, sign in as test user A, and send test user B's id. If you get B's data back, or change B's rows, the function trusts the request.
The fix
The simplest fix is to stop using the service key here: make a client that carries the caller's own token, so row level security applies exactly as it does in the browser.
import { createClient } from 'npm:@supabase/supabase-js@2';
Deno.serve(async (req) => {
const authHeader = req.headers.get('Authorization');
if (!authHeader) return new Response('Unauthorized', { status: 401 });
// Acts as the caller: your RLS policies decide what it can see
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader } } },
);
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) return new Response('Unauthorized', { status: 401 });
// The id comes from the verified token, never from the body
const { data } = await supabase.from('profiles')
.select('email, phone, credits').eq('id', user.id).single();
return Response.json(data);
});
If the function genuinely needs the service key, verify the caller with admin.auth.getUser(token) first and use user.id from the result. If admins may act for other users, check the admin role, then accept the target id.
Code findings come with the file, the line and a one-line fix in the report, for your engineer to make. PatchLabs never pushes to your repo.
How PatchLabs finds it
[SCAN] get-profile/index.ts:8 userId taken from req.json() LEAK used with the service key [FIX] id from auth.getUser()
PatchLabs Code reads Edge Functions and server API routes for user ids taken from the request body or query and used in a query or a privileged call. It doesn't call your functions. The report gives the file and line where the id is read.
When the pattern is intended
Admin tools that act on other users, after checking the caller is an admin, and public lookups that return only public fields (a seller's display name) are fine.
Why it’s rated high
High: it reads or changes another user's data with nothing more than a valid account and their id.
Related findings
Edge function open without sign-in while using the service key
A server function holding the key that ignores every rule, with nothing checking who is calling it.
PL-021Payment webhook doesn't verify its signature
Anyone can send your app a fake 'payment succeeded' event and get the paid plan for free.
PL-012 · comingStrangers can change or delete other people's rows
A write policy checks that someone is signed in, not that the row is theirs. Anyone who signs up can edit or wipe it.
Guides
Free check
Does your app have PL-022?
Paste your public repo. PatchLabs Code reads it for this and every other code finding, and the Lab rebuilds your database to test who can read what.
Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.