| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
- /**
- * SSRF Protection: Validates URLs to prevent Server-Side Request Forgery attacks
- * Blocks internal network addresses, cloud metadata endpoints, and non-HTTP protocols
- */
- export function isValidExternalUrl(urlString: string): { valid: boolean; error?: string } {
- try {
- const url = new URL(urlString);
-
- // Only allow HTTP and HTTPS protocols
- if (!['http:', 'https:'].includes(url.protocol)) {
- return { valid: false, error: 'Only HTTP and HTTPS protocols are allowed' };
- }
-
- const hostname = url.hostname.toLowerCase();
-
- // Block localhost and loopback
- if (['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(hostname)) {
- return { valid: false, error: 'Localhost addresses are not allowed' };
- }
-
- // Block private IP ranges (RFC 1918)
- if (/^10\./.test(hostname) ||
- /^172\.(1[6-9]|2[0-9]|3[01])\./.test(hostname) ||
- /^192\.168\./.test(hostname)) {
- return { valid: false, error: 'Private IP addresses are not allowed' };
- }
-
- // Block link-local addresses
- if (/^169\.254\./.test(hostname) || /^fe80:/i.test(hostname)) {
- return { valid: false, error: 'Link-local addresses are not allowed' };
- }
-
- // Block cloud metadata endpoints
- const blockedHostnames = [
- '169.254.169.254', // AWS/GCP/Azure metadata
- 'metadata.google.internal', // GCP metadata
- 'metadata.internal', // Generic cloud metadata
- '100.100.100.200', // Alibaba Cloud metadata
- ];
- if (blockedHostnames.includes(hostname)) {
- return { valid: false, error: 'Cloud metadata endpoints are not allowed' };
- }
-
- // Block internal/private domain patterns
- if (hostname.endsWith('.internal') ||
- hostname.endsWith('.local') ||
- hostname.endsWith('.localhost')) {
- return { valid: false, error: 'Internal domain names are not allowed' };
- }
-
- return { valid: true };
- } catch {
- return { valid: false, error: 'Invalid URL format' };
- }
- }
- /**
- * Authentication helper: Verifies JWT and returns user claims
- * Returns null if authentication fails
- */
- export async function verifyAuth(req: Request): Promise<{ userId: string; email?: string } | null> {
- const authHeader = req.headers.get('Authorization');
-
- if (!authHeader?.startsWith('Bearer ')) {
- return null;
- }
-
- try {
- const supabase = createClient(
- Deno.env.get('SUPABASE_URL')!,
- Deno.env.get('SUPABASE_ANON_KEY')!,
- { global: { headers: { Authorization: authHeader } } }
- );
-
- const token = authHeader.replace('Bearer ', '');
- const { data, error } = await supabase.auth.getUser(token);
-
- if (error || !data?.user) {
- return null;
- }
-
- return {
- userId: data.user.id,
- email: data.user.email
- };
- } catch {
- return null;
- }
- }
- /**
- * Verifies if the authenticated user is a super user
- */
- export async function verifySuperUser(req: Request): Promise<boolean> {
- const auth = await verifyAuth(req);
- if (!auth) return false;
-
- try {
- const supabase = createClient(
- Deno.env.get('SUPABASE_URL')!,
- Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
- );
-
- const { data, error } = await supabase.rpc('is_super_user', { user_email: auth.email });
-
- if (error) {
- console.error('Error checking super user status:', error);
- return false;
- }
-
- return data === true;
- } catch {
- return false;
- }
- }
- /**
- * Validates a cron job secret for internal service-to-service calls
- * Cron jobs should use a shared secret for authentication
- * SECURITY: Fails closed - requires CRON_SECRET to be configured
- */
- export function validateCronSecret(req: Request): boolean {
- const cronSecret = req.headers.get('x-cron-secret');
- const expectedSecret = Deno.env.get('CRON_SECRET');
-
- // SECURITY: Fail closed - require explicit configuration
- if (!expectedSecret) {
- console.error('CRON_SECRET environment variable not configured - denying access');
- return false;
- }
-
- // Constant-time comparison to prevent timing attacks
- if (cronSecret?.length !== expectedSecret.length) {
- return false;
- }
-
- return cronSecret === expectedSecret;
- }
- /**
- * Check if request is from internal Supabase service (edge function to edge function)
- */
- export function isInternalCall(req: Request): boolean {
- // Check for service role key in authorization
- const authHeader = req.headers.get('Authorization');
- const serviceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY');
-
- if (authHeader && serviceRoleKey) {
- return authHeader.includes(serviceRoleKey);
- }
-
- return false;
- }
|