PL-022highCode

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 (vulnerable)
// 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

check-pl-022.sh
# 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.

supabase/functions/get-profile/index.ts (fixed)
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

code scan · report excerpt
[SCAN]    get-profile/index.ts:8
          userId taken from req.json() LEAK
          used with the service key
[FIX]     id from auth.getUser()
synthetic exampleReport excerpt · read from the repo, not lab-proven

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.

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.

Free for your first appReads your public repo onlyOr run the free static RLS check

Last reviewed 10 Nov 2026. Finding IDs are stable: link to them freely. Spotted a mistake? Tell us.