Why Edge Functions Need Hardening
Supabase Edge Functions run on Deno Deploy and often serve as the secure bridge between your client application and privileged backend operations. They frequently use the service role key to bypass RLS for administrative tasks. A vulnerability in an Edge Function can be just as devastating as a database misconfiguration.
Authentication and Authorization
Verify the JWT
Every Edge Function that handles user requests must verify the incoming JWT:
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 now verified, proceed with logic
});
Do not just decode the JWT yourself without verification. Always call getUser() which validates the token against Supabase Auth.
Authorize the Action
Authentication alone is not enough. Verify the user has permission for the specific action:
// Check user has credits before performing a scan
const { data: profile } = await supabaseAdmin
.from('profiles')
.select('credits')
.eq('id', user.id)
.single();
if (!profile || profile.credits < 1) {
return new Response(JSON.stringify({ error: 'Insufficient credits' }), {
status: 403,
headers: { 'Content-Type': 'application/json' },
});
}
Input Validation
Never trust client input. Validate everything:
Deno.serve(async (req: Request) => {
if (req.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
let body: unknown;
try {
body = await req.json();
} catch {
return new Response(JSON.stringify({ error: 'Invalid JSON' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
// Validate expected fields
if (typeof body !== 'object' || body === null) {
return new Response(JSON.stringify({ error: 'Invalid request body' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
const { url } = body as Record<string, unknown>;
if (typeof url !== 'string' || !url.startsWith('https://')) {
return new Response(JSON.stringify({ error: 'Invalid URL' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
// Safe to proceed with validated input
});
Protecting the Service Role Key
The service role key bypasses RLS entirely. Treat it with extreme care:
- Never return it to the client -- Not in responses, error messages, or logs
- Use it only in Edge Functions, never in client-side code
- Create a separate admin client and use it only for operations that truly need elevated privileges
// Admin client for privileged operations
const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
// Regular client for user-scoped operations
const supabaseUser = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader } } }
);
CORS Configuration
Every Edge Function should set proper CORS headers:
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://yourdomain.com', // Not '*'
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'authorization, content-type, x-client-info',
};
Deno.serve(async (req: Request) => {
// Handle preflight
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
// ... function logic
return new Response(JSON.stringify(result), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
});
Avoid using '*' for the Access-Control-Allow-Origin header in production. Specify your exact domain.
Rate Limiting
Edge Functions do not have built-in rate limiting. Implement your own:
// Simple rate limiting using Supabase table
async function checkRateLimit(userId: string, action: string): Promise<boolean> {
const windowStart = new Date(Date.now() - 60 * 1000).toISOString(); // 1 min window
const { count } = await supabaseAdmin
.from('rate_limits')
.select('*', { count: 'exact', head: true })
.eq('user_id', userId)
.eq('action', action)
.gte('created_at', windowStart);
return (count ?? 0) < 10; // Max 10 requests per minute
}
Error Handling
Never leak internal details in error responses:
try {
// ... operation
} catch (err) {
// Log full error server-side
console.error('Internal error:', err);
// Return generic error to client
return new Response(
JSON.stringify({ error: 'An internal error occurred' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
Never expose stack traces, database error messages, or internal paths to the client. These give attackers information about your infrastructure.
Environment Variables
Store all secrets in Supabase Edge Function secrets, never in code:
supabase secrets set STRIPE_SECRET_KEY=sk_live_...
supabase secrets set RESEND_API_KEY=re_...
Access them via Deno.env.get() at runtime. Never commit .env files containing production secrets.
Automated Scanning
AuditYour.app can test your Edge Functions for common vulnerabilities including missing authentication checks, CORS misconfigurations, and exposed error details. Regular automated scanning catches issues before they reach production.
Scan your app for this vulnerability
AuditYourApp automatically detects security misconfigurations in Supabase and Firebase projects. Get actionable remediation in minutes.
Run Free ScanRelated
guides
Complete Guide to Supabase Row Level Security
Deep dive into RLS policies, patterns, and common pitfalls
guides
Supabase Anonymous Key Security
Understanding anon key risks and proper usage
guides
Securing Supabase RPC Functions
How to properly secure database functions exposed via RPC
guides
Supabase Database Security Best Practices
Comprehensive Postgres/Supabase DB hardening guide