Supabase12 items

Supabase Edge Function Security Checklist

Checklist for securing Edge Functions

Last updated 2026-01-15

Quick Checklist

  • 1Validate the Authorization header on every request
  • 2Verify JWT and extract user context before processing
  • 3Use the service_role client only for privileged operations
  • 4Validate and sanitize all request body inputs
  • 5Set appropriate CORS headers
  • 6Handle errors without leaking internal details
  • 7Store secrets using Supabase Secrets, not in code
  • 8Implement rate limiting for public-facing functions
  • 9Use parameterized queries to prevent SQL injection
  • 10Log security-relevant events for auditing
  • 11Set function-level permissions and invocation policies
  • 12Test with malformed and oversized payloads

Supabase Edge Function Security Checklist

Supabase Edge Functions run on the Deno runtime and serve as the server-side logic layer for your application. Because they can use the service_role key to bypass RLS, they represent a critical security boundary. This checklist covers how to write secure Edge Functions.

1. Authentication and Authorization

Every Edge Function that handles user-specific data must validate the JWT from the Authorization header:

import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

Deno.serve(async (req: Request) => {
  const authHeader = req.headers.get('Authorization');
  if (!authHeader) {
    return new Response(
      JSON.stringify({ error: 'Missing authorization header' }),
      { status: 401, headers: { 'Content-Type': 'application/json' } }
    );
  }

  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(
      JSON.stringify({ error: 'Invalid token' }),
      { status: 401, headers: { 'Content-Type': 'application/json' } }
    );
  }

  // User is authenticated; proceed with user.id
});

Do not use getSession() for authorization. The session's JWT can be tampered with on the client side. Always use getUser() which validates the token against the Supabase Auth server.

2. Service Role Key Usage

The service_role key bypasses all RLS policies. Create a separate Supabase client for privileged operations and use it sparingly:

// Only create this when you need to bypass RLS
const adminClient = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);

// Always verify the user's permission BEFORE using adminClient
if (user.app_metadata.role !== 'admin') {
  return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403 });
}

// Now safe to perform privileged operation
const { data } = await adminClient.from('admin_config').select('*');

Never pass the service_role key in responses, logs, or error messages.

3. Input Validation

Validate every field from the request body. Edge Functions receive arbitrary input from the internet:

const body = await req.json().catch(() => null);
if (!body) {
  return new Response(JSON.stringify({ error: 'Invalid JSON body' }), { status: 400 });
}

const { url, projectId } = body;

// Validate URL format
if (typeof url !== 'string' || !url.startsWith('https://')) {
  return new Response(JSON.stringify({ error: 'Invalid URL' }), { status: 400 });
}

// Validate projectId is a UUID
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(projectId)) {
  return new Response(JSON.stringify({ error: 'Invalid project ID' }), { status: 400 });
}

Be especially careful with inputs that are used in:

  • Database queries (SQL injection)
  • HTTP requests to external services (SSRF)
  • File system operations (path traversal)
  • Template rendering (injection attacks)

4. CORS Configuration

Set CORS headers on every response, including error responses:

const corsHeaders = {
  'Access-Control-Allow-Origin': 'https://yourapp.com',
  'Access-Control-Allow-Methods': 'POST, OPTIONS',
  'Access-Control-Allow-Headers': 'authorization, content-type, x-client-info',
};

// Handle preflight
if (req.method === 'OPTIONS') {
  return new Response(null, { status: 204, headers: corsHeaders });
}

Avoid using Access-Control-Allow-Origin: * in production. Restrict it to your application's domain.

5. Secret Management

Store all secrets (API keys, webhook signing secrets, encryption keys) using Supabase secrets:

supabase secrets set STRIPE_SECRET_KEY=sk_live_...
supabase secrets set WEBHOOK_SECRET=whsec_...

Access them via Deno.env.get() at runtime. Never hardcode secrets in function source code.

6. SQL Injection Prevention

When using raw SQL in Edge Functions (via supabase.rpc() or direct queries), always use parameterized queries:

// DANGEROUS: String interpolation
const { data } = await adminClient.rpc('search_users', { query: userInput });
// Make sure the database function uses parameterized queries internally

// SAFE: Use the Supabase client's built-in parameterization
const { data } = await supabase
  .from('users')
  .select('*')
  .ilike('name', `%${sanitizedInput}%`);

7. Error Handling

Never expose internal error details to the client:

try {
  // Business logic
} catch (error) {
  // Log the full error for debugging
  console.error('Function error:', error);
  // Return a generic message to the client
  return new Response(
    JSON.stringify({ error: 'Internal server error' }),
    { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
  );
}

Use AuditYour.app to scan your Supabase project and ensure your Edge Functions are properly secured.

Scan your app for this vulnerability

AuditYourApp automatically detects security misconfigurations in Supabase and Firebase projects. Get actionable remediation in minutes.

Run Free Scan