Supabase Edge Functions: Common Security Mistakes in AI-Generated Code - VibeDoctor 
← All Articles ⚛️ Framework-Specific Guides Critical

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). 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 equivalent to a database superuser credential.

According to GitGuardian's 2024 report, Supabase service keys are among the top 10 most commonly leaked cloud credentials. Once exposed, an attacker can read, modify, and delete every row in every table. The Apiiro 2024 Software Supply Chain Security report found that 48% of applications with Supabase integration had at least one instance of the service key being used where the anon key would suffice.

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

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 '@supabase/supabase-js';

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": "[email protected]", "role": "admin", "credits": 999999 }

// ✅ GOOD - Validate every field, whitelist allowed values
import { z } from 'https://deno.land/x/[email protected]/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 (vibedoctor.io) scan your codebase for service key exposure in client-side code, missing input validation, CORS misconfigurations, and other security patterns specific to Supabase projects. 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. Without RLS policies, the anon key grants no access to any table. 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?

Supabase has global rate limits on their infrastructure, but they are generous and not designed to protect individual functions from abuse. 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.

Scan your codebase for this issue - free

VibeDoctor checks for SEC-001, SEC-006, SEC-010, SEC-013 and 128 other issues across 15 diagnostic areas.

SCAN MY APP →
← Back to all articles View all 129+ checks →