Example finding How it works Coverage PENTEST METHODOLOGY DOCS PRICING FAQ MCP CONTACT LOG IN SIGN UP →

Supabase Edge Function with verify_jwt disabled: how to fix it

FIX / EDGE FUNCTION AUTH / 9 MIN READ

Set verify_jwt = true for the function in supabase/config.toml, deploy the function again, and check the signed-in user inside the code. A Supabase Edge Function with verify_jwt = false accepts a request from any caller on the internet. The function URL is public. Anyone who knows the URL can run your code with your secrets.

This guide shows how to confirm that a function is open, how to close it, and how to read the logs for misuse. It also covers the one case where verify_jwt = false is correct.

What does “verify_jwt disabled” mean for an Edge Function?

It means that the Supabase platform does not check the token before your function code runs. Every request reaches your handler, with or without a valid token.

The Supabase function auth docs say: “The default is verify_jwt = true, so the platform validates the JWT before your handler runs.” The same page warns that turning the check off to clear a 401 during development removes the platform check.

A deployed function has a public URL. The form is https://YOUR-PROJECT-REF.supabase.co/functions/v1/FUNCTION-NAME. A browser can show this URL in the network tab. Your frontend code often contains it too. A bot does not need to guess it.

The risk depends on what the function does. A function that returns a public value has little risk. A function with access to a secret has high risk. These are common cases:

Function does Risk when open
Calls a paid API, such as an LLM Anyone spends your money
Sends email or SMS Anyone sends messages from your domain
Uses the service_role key Anyone reads or writes any row
Creates a payment or a refund Anyone starts payment actions
Deletes data Anyone deletes your data

AI coding tools often turn the check off. A function returns 401 Invalid JWT during a test. The tool, or the developer, fixes the error by setting verify_jwt = false. The error disappears, but so does the protection. The real cause of the 401 was the caller, which sent no token or a wrong token.

How do I confirm that my function is open?

Call the function without any token. Do this from a terminal, not from your app.

curl -i -X POST "https://YOUR-PROJECT-REF.supabase.co/functions/v1/FUNCTION-NAME" \
  -H "Content-Type: application/json" \
  -d '{}'

Read the status line of the response.

Response Meaning
401 from the platform The platform check is on
200 or an error from your own code The platform check is off, or the function is public by design
404 The function name is wrong

A 401 is the result you want for a function that needs a user. Use a test body that does no harm. Do not test a function that sends email or deletes data with a real payload.

Then check your project files. Do these searches in the repository:

  1. Search supabase/config.toml for verify_jwt. Each verify_jwt = false line is a function without the platform check.
  2. Search your scripts and CI files for no-verify-jwt. A deploy command can also turn the check off, so a clean config.toml does not prove that the function is protected.
  3. List the functions with supabase functions list. Compare the list with config.toml. A function that is not in the file uses the default.
  4. Open each function and read its first lines. Look for a check of the user.

Finally, test with the anon key only. The anon key is public, so an attacker has it. Send it as the token:

curl -i -X POST "https://YOUR-PROJECT-REF.supabase.co/functions/v1/FUNCTION-NAME" \
  -H "Authorization: Bearer YOUR_ANON_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

If this request succeeds on a function that must need a signed-in user, the function is open. The platform check is not enough. This is the second half of the problem, and the next section covers it.

Why is verify_jwt = true not enough?

The platform check proves that the token is a valid JWT for your project. It does not prove that a user signed in. The anon key is a signed token that belongs to your project. Every visitor of your site can read it. See the anon key guide for what the key allows.

For this reason, a function that needs a user must check the user in its own code. Read the Supabase JWT docs for how the verification works. The docs recommend that you rely on supabase.auth.getClaims() or another high-quality JWT library. For tokens from a shared secret, the docs recommend that you verify the user token with the Auth server.

The example below uses getUser. It sends the token of the caller to the Auth server, which checks it:

import { createClient } from "npm:@supabase/supabase-js@2";

Deno.serve(async (req) => {
  const authHeader = req.headers.get("Authorization") ?? "";
  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 caller is a signed-in user. Run the action for user.id.
  return new Response(JSON.stringify({ ok: true, userId: user.id }), {
    headers: { "Content-Type": "application/json" },
  });
});

The anon key is not a user. A request with only the anon key has no user, so getUser returns an error and the function answers 401.

Warning: Do not trust a user id from the request body. A caller can send any id. Take the id from the verified user object.

How do I fix an open Edge Function now?

Do the steps in this order. Steps 1 to 3 stop the exposure. The later steps remove the cause.

  1. Turn the platform check on. Open supabase/config.toml. Remove the verify_jwt = false line for the function, or set it to true:

    [functions.your-function]
    verify_jwt = true
    
  2. Remove the deploy flag. Search your scripts and CI files for no-verify-jwt and remove it. A deploy command that has the flag can override the file. Read the function deploy docs for how config.toml settings apply to a deploy.

  3. Deploy the function again. Run supabase functions deploy FUNCTION-NAME. A setting in a file has no effect on a deployed function until you deploy.

  4. Test again with curl. Repeat the call without a token. The response must be 401.

  5. Add the user check in the code. Use the getUser example above. Test with the anon key as the token. The response must also be 401.

  6. Update the caller. If the caller is your frontend, call the function with the Supabase client. The client sends the user token for you:

    const { data, error } = await supabase.functions.invoke("FUNCTION-NAME", {
      body: { prompt },
    });
    
  7. Restrict the origin. Set the CORS header to your real domain. Do not use * for a function that spends money or changes data.

  8. Move secrets that were exposed. If the logs show calls that you did not make, rotate the keys that the function used. See the next sections.

Caution: Step 1 can break a caller that sends no token. A broken caller is correct. Fix the caller. Do not turn the check off again.

What if the function must be public?

Some functions must accept calls without a user token. A webhook from a payment provider is the usual case. The provider cannot sign in as a user. For these functions, verify_jwt = false is correct. You then need another proof of identity.

Use the signature of the sender. Most providers sign each webhook with a secret. Your code must check the signature before it acts.

A safe public function follows these rules:

  1. Check the signature first. Read the signature header and compare it with the secret of the provider. Reject the request when the check fails. Use the verification function of the provider library, not your own string compare.
  2. Check the timestamp. Reject events that are too old. This stops replay of an old valid request.
  3. Store the secret as a function secret. Do not put it in the code. The Supabase secrets docs show the command supabase secrets set NAME=value.
  4. Use the least privilege. Do not give a webhook function the service_role key if one table update is enough. See the service_role key guide.
  5. Handle repeated events. Providers send the same event more than once. Store the event id and skip the duplicates.
  6. Limit the response. Return a short status. Do not return data about your system.

Keep the verify_jwt = false line only for this function. Add a comment in config.toml that names the reason. The next reader then sees that the setting is on purpose.

A function that sends public content, for example a public image resize, can also be open. Put a limit on its input size and its call rate. Without a limit, an open function is still a cost risk.

How do I check whether someone used the open function?

Read the logs of the function and the logs of the services that it calls. Use the dates from your last deploy of the open version.

  1. Open the function logs. In the Supabase dashboard, open Edge Functions, select the function, and read the Logs and Invocations views.
  2. Compare the call count with your real traffic. Look for days with many more calls than your users could make.
  3. Look at the status codes. A burst of 200 responses from unknown sources is the main sign of misuse.
  4. Read the request details. Look for calls without a user token, calls with unusual bodies, and calls from one address that repeat.
  5. Check the downstream service. Open the usage page of each paid API that the function calls. Look for cost that you did not cause.
  6. Check the database. If the function wrote data, look at the newest rows and at changes in the tables that it touches. Compare them with your normal use.
  7. Check the email or SMS logs. If the function sent messages, read the sending logs for messages that you did not plan.

If the function used the service_role key, treat the exposure as serious. The function ran with full database access. An attacker could have read or changed any row that your code allowed. Read the Supabase RLS guide to limit the damage of future mistakes.

Note: The logs can be incomplete. They have a retention time, and they do not show all request contents. If you find no sign of misuse, you still cannot prove that no misuse happened. For a function with a paid key or a privileged key, rotate the key anyway.

For a function that calls a paid LLM API, read how to fix an exposed OpenAI key. The same checks for usage and spend limits apply.

How do I stop the next open function?

Use controls that make the safe state the normal state.

  1. Keep the default. Do not set verify_jwt = false to clear an error. Find the cause of the 401 first. The usual causes are a missing Authorization header, an expired session, and a token from another project.
  2. Add a rule for AI tools. Write in the project instructions: “Never set verify_jwt to false without a signature check in the function.”
  3. Search in each review. Search each pull request for verify_jwt, no-verify-jwt, and service_role.
  4. Keep one auth helper. Put the getUser check in one shared file. Each function imports it. A new function then starts with the check.
  5. Use the least privilege. Create the Supabase client with the token of the caller, so Row Level Security applies. Use the service_role key only when no other way works.
  6. Add limits. Put a rate limit per user and an input size limit in each function that costs money. See API abuse protection.
  7. Test after each deploy. Add a script that calls each function URL without a token and with only the anon key. The build fails when any protected function answers 200.
  8. Scan the live app. A scan tests the deployed URLs, not the files.

When should I get help?

Get help in these cases:

  • The logs show many unknown calls, and the function used the service_role key. Ask a security engineer to review the data of the project.
  • The function sent email or SMS to real people. Contact your email provider, and check whether you have a duty to tell the recipients.
  • You cannot tell which functions are open. Run a scan of the deployed app. The scan lists the URLs that answer without a valid token.
  • A payment function was open. Check each payment in the dashboard of the provider and contact the provider support.

After you finish the fix, run the two curl tests once more. The request without a token and the request with only the anon key must both return 401.

Common questions

What is the default value of verify_jwt?
The default is true. The Supabase docs say the platform then validates the JWT before your handler runs.
Is verify_jwt true enough to protect a function?
No. The anon key is also a JWT, and it is public. Check the signed-in user inside the function.
Can a webhook function keep verify_jwt false?
Yes, if the function checks the signature of the sender. Without a signature check, anyone can send a fake event.
Why do I get a 401 Invalid JWT error?
The caller sent no token, an expired token, or a token from another project. Fix the caller. Do not turn the check off.
Do I need to rotate keys after I find the function open?
Yes, if the function used a paid API or a privileged key and the logs show unknown calls. Rotate those keys first.

Test your function urls

A VibeEval scan calls the Edge Function URLs that your app uses and reports the functions that answer without a valid user token.

14-day free trial · No credit card · Cancel anytime

SCAN YOUR APP →