← All Articles ⚛️ Framework-Specific Guides Critical By Nishant Kumar Verma Updated

Supabase Edge Functions: Common Security Mistakes in AI-Generated Code

AI tools generate Supabase Edge Functions with exposed service role keys, no input validation, and missing CORS config. Here is how to fix them.

SEC-001 SEC-006 SEC-010 SEC-013

Quick Answer

AI tools generate Supabase Edge Functions that use the service role key (bypasses all Row Level Security), skip input validation, return full database rows including sensitive fields, and lack proper CORS configuration. The service role key is the master key to your database - if exposed in client-side code or logs, an attacker has full read/write access to every table. Always use the anon key with RLS for user requests and validate every input with Zod.

The Service Key Problem

Supabase provides two keys: the anon key (respects Row Level Security) and the service role key (bypasses all RLS policies). Newer projects call them the publishable key and the secret key; the legacy keys used below still work, but Supabase is deprecating them by the end of 2026. AI code generators almost always use the service role key because it "just works" - no RLS policies to configure, no permission errors to debug. But the service role key is an admin credential: Supabase says it has full access to your project's data and skips every RLS policy.

A Supabase service key bypasses row level security. Once exposed, an attacker can read, modify, and delete every row in every table. A common mistake is using the service key in places where the anon key would do.

// ❌ BAD - Service role key in Edge Function (bypasses all RLS)
import { createClient } from 'npm:@supabase/supabase-js@2';

const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // Full database access!
);

Deno.serve(async (req) => {
  const { userId } = await req.json();
  // No auth check - anyone can read any user's data
  const { data } = await supabase
    .from('users')
    .select('*')  // Returns password_hash, internal fields
    .eq('id', userId);
  return new Response(JSON.stringify(data));
});
// ✅ GOOD - User's JWT token creates an authenticated client with RLS
import { createClient } from 'npm:@supabase/supabase-js@2';

Deno.serve(async (req) => {
  // Extract user's JWT from the Authorization header
  const authHeader = req.headers.get('Authorization');
  if (!authHeader) {
    return new Response('Unauthorized', { status: 401 });
  }

  // Create a client that respects RLS using the user's token
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!,
    { global: { headers: { Authorization: authHeader } } }
  );

  // RLS policies automatically restrict to the user's own data
  const { data } = await supabase
    .from('profiles')
    .select('id, name, avatar_url');  // Only safe fields

  return new Response(JSON.stringify(data));
});

When the Service Key IS Needed

The service role key has legitimate uses in Edge Functions, but only for operations that should bypass RLS by design:

Use Case Key to Use Why
User reads their own data Anon key + user JWT RLS restricts access to own rows
User updates their profile Anon key + user JWT RLS ensures they edit only their own data
Webhook from Stripe/GitHub Service role key External webhooks have no user JWT
Admin batch operation Service role key Admin operations cross user boundaries
Scheduled cleanup job Service role key Background jobs have no user context

Input Validation in Edge Functions

Edge Functions receive raw HTTP requests. AI tools generate code that trusts every field in the request body without validation, which enables injection attacks and unexpected behavior.

// ❌ BAD - No validation, trusts all input
Deno.serve(async (req) => {
  const { email, role, credits } = await req.json();
  await supabase.from('users').insert({ email, role, credits });
  return new Response('Created');
});
// Attacker sends: { "email": "a@b.com", "role": "admin", "credits": 999999 }

// ✅ GOOD - Validate every field, whitelist allowed values
import { z } from 'https://deno.land/x/zod@v3.22.4/mod.ts';

const createUserSchema = z.object({
  email: z.string().email().max(255),
  // 'role' and 'credits' are NOT accepted from client
});

Deno.serve(async (req) => {
  const body = await req.json();
  const data = createUserSchema.parse(body);

  await supabase.from('users').insert({
    email: data.email,
    role: 'user',     // Server-defined default
    credits: 0,       // Server-defined default
  });
  return new Response(JSON.stringify({ success: true }));
});

CORS Configuration Mistakes

AI-generated Edge Functions either skip CORS entirely (breaking browser requests) or use * for all origins (allowing any website to call your API):

// ❌ BAD - Allows any origin
const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': '*',
  'Access-Control-Allow-Headers': '*',
};

// ✅ GOOD - Restrict to your domains
const ALLOWED_ORIGINS = [
  'https://yourdomain.com',
  'https://app.yourdomain.com',
];

function getCorsHeaders(req: Request) {
  const origin = req.headers.get('Origin') ?? '';
  const allowedOrigin = ALLOWED_ORIGINS.includes(origin) ? origin : '';
  return {
    'Access-Control-Allow-Origin': allowedOrigin,
    'Access-Control-Allow-Methods': 'POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
  };
}

Edge Function Security Checklist

  1. Use anon key + user JWT for user-facing requests - let RLS do the authorization
  2. Reserve service role key for webhooks, admin jobs, and background tasks only
  3. Validate all input with Zod - never trust request body fields
  4. Never accept role/permission fields from the client
  5. Select specific columns - never return SELECT *
  6. Configure CORS properly - whitelist your domains, not *
  7. Verify webhook signatures - Stripe, GitHub, and others sign their payloads
  8. Handle errors safely - never return stack traces or database errors to the client

Tools like VibeDoctor's Vibe Check flag hardcoded keys and tokens, including a service role key pasted into source code, and variables with SECRET, PRIVATE, or PASSWORD in their name exposed through prefixes like NEXT_PUBLIC_ or VITE_. Review the CORS headers and input validation in your Edge Functions by hand. Free to sign up.

FAQ

Is it safe to expose the Supabase anon key?

Yes. The anon key is designed to be used in client-side code. It only grants access that your Row Level Security policies allow. On a table with RLS enabled and no policies, the anon key gets no access at all; on a table with RLS disabled, it can read and write every row. With properly configured RLS, it lets authenticated users access only their own data. The danger is when you use the service role key instead, which bypasses all RLS.

How do I know if my Edge Function is using the wrong key?

Search your code for SUPABASE_SERVICE_ROLE_KEY. If it appears in any Edge Function that handles user requests (not webhooks or admin jobs), it should be replaced with the anon key + user JWT pattern. The service key should only appear in server-side code that intentionally needs to bypass RLS.

Can I use Row Level Security with Edge Functions?

Yes. When you create a Supabase client with the anon key and pass the user's JWT in the Authorization header, all queries respect your RLS policies automatically. This is the recommended pattern for user-facing Edge Functions. It means your function does not need to manually check permissions - the database enforces them.

What if my Edge Function needs both RLS and service-level access?

Create two Supabase clients: one with the anon key + user JWT (for user-scoped queries) and one with the service role key (for admin operations within the same function). Use the user client for reading/writing user data and the service client only for cross-user operations like incrementing global counters or sending notifications.

Do Supabase Edge Functions have built-in rate limiting?

Not for incoming requests. Supabase rate limits calls one Edge Function makes to another, but inbound requests to your Edge Functions are not subject to that limit. Add application-level rate limiting inside your Edge Functions for sensitive operations like authentication, payments, and data writes. Use a counter in your database or Redis for cross-request tracking; Supabase's own rate limiting example uses Upstash Redis.

Diagnose your codebase - free

VibeDoctor checks for SEC-001, SEC-006, SEC-010, SEC-013 and 148 other issues across 21 diagnostic areas - security, performance, code quality, and more.

SCAN MY APP →