Просмотр исходного кода

security: restrict CORS origins for WebUI-facing Edge Functions

Replace wildcard CORS (*) with environment-controlled origins for security.

Changes:
- Created shared CORS helper in _shared/cors.ts
- Uses EDGE_FUNCTION_ALLOWED_ORIGINS env variable
- Format: comma-separated list (e.g., "https://shopcall.ai,http://192.168.2.112:8081")
- Updated all WebUI-facing functions:
  * custom-content-* (7 functions)
  * api, auth
  * api-key-management
  * complete-shoprenter-install
  * scraper-management
  * validate-shoprenter-hmac

Security improvement:
- Before: Allow-Origin: * (any domain)
- After: Allow-Origin: configured trusted domains only
- Prevents unauthorized frontend access to Edge Functions

Internal/webhook functions (not called by WebUI) remain unchanged:
- custom-content-process (internal only)
- OAuth callbacks
- Webhooks
- Scheduled sync functions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh 4 месяцев назад
Родитель
Сommit
e18de40ed6

+ 40 - 0
supabase/functions/_shared/cors.ts

@@ -0,0 +1,40 @@
+/**
+ * CORS Configuration Helper
+ *
+ * Provides secure CORS headers based on environment configuration.
+ * Uses EDGE_FUNCTION_ALLOWED_ORIGINS environment variable to restrict origins.
+ */
+
+/**
+ * Get CORS headers with allowed origins from environment
+ *
+ * @param additionalMethods - Additional HTTP methods to allow (default: GET, POST, OPTIONS)
+ * @returns CORS headers object
+ */
+export function getCorsHeaders(additionalMethods: string[] = []): Record<string, string> {
+  // Get allowed origins from environment variable
+  // Format: comma-separated list, e.g., "https://shopcall.ai,http://192.168.2.112:8081"
+  const allowedOriginsEnv = Deno.env.get('EDGE_FUNCTION_ALLOWED_ORIGINS') || '*';
+
+  // Default methods
+  const defaultMethods = ['GET', 'POST', 'OPTIONS'];
+  const allMethods = [...new Set([...defaultMethods, ...additionalMethods])];
+
+  return {
+    'Access-Control-Allow-Origin': allowedOriginsEnv,
+    'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
+    'Access-Control-Allow-Methods': allMethods.join(', '),
+  };
+}
+
+/**
+ * Handle CORS preflight request
+ *
+ * @param additionalMethods - Additional HTTP methods to allow
+ * @returns Response for OPTIONS request
+ */
+export function handleCorsPreflightRequest(additionalMethods: string[] = []): Response {
+  return new Response(null, {
+    headers: getCorsHeaders(additionalMethods)
+  });
+}

+ 1 - 0
supabase/functions/api-key-management/index.ts

@@ -9,6 +9,7 @@
 import { createClient } from "https://esm.sh/@supabase/supabase-js@2.39.7";
 import { createClient } from "https://esm.sh/@supabase/supabase-js@2.39.7";
 import { wrapHandler } from "../_shared/error-handler.ts";
 import { wrapHandler } from "../_shared/error-handler.ts";
 import { generateApiKey, hashApiKey } from "../_shared/api-key-auth.ts";
 import { generateApiKey, hashApiKey } from "../_shared/api-key-auth.ts";
+import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
 
 
 const FUNCTION_NAME = "api-key-management";
 const FUNCTION_NAME = "api-key-management";
 
 

+ 4 - 6
supabase/functions/api/index.ts

@@ -2,17 +2,15 @@ import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
 import { wrapHandler } from '../_shared/error-handler.ts'
 import { wrapHandler } from '../_shared/error-handler.ts'
 
 
-const corsHeaders = {
-  'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
-  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
-}
+import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
+
+const corsHeaders = getCorsHeaders(['PUT', 'DELETE']);
 
 
 // Handle OPTIONS requests BEFORE wrapping with error handler to avoid timeouts
 // Handle OPTIONS requests BEFORE wrapping with error handler to avoid timeouts
 serve(async (req) => {
 serve(async (req) => {
   // Fast-path for OPTIONS requests
   // Fast-path for OPTIONS requests
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
-    return new Response('ok', { headers: corsHeaders })
+    return handleCorsPreflightRequest(['PUT', 'DELETE'])
   }
   }
 
 
   // Wrap all other requests with error handler
   // Wrap all other requests with error handler

+ 3 - 5
supabase/functions/auth/index.ts

@@ -1,11 +1,9 @@
 import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
 import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
 import { wrapHandler, logError } from '../_shared/error-handler.ts'
 import { wrapHandler, logError } from '../_shared/error-handler.ts'
+import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
 
 
-const corsHeaders = {
-  'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
-}
+const corsHeaders = getCorsHeaders();
 
 
 function generateOTP(): string {
 function generateOTP(): string {
   return Math.floor(100000 + Math.random() * 900000).toString()
   return Math.floor(100000 + Math.random() * 900000).toString()
@@ -77,7 +75,7 @@ async function sendOTPEmail(email: string, otp: string, userName: string): Promi
 
 
 serve(wrapHandler('auth', async (req) => {
 serve(wrapHandler('auth', async (req) => {
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
-    return new Response('ok', { headers: corsHeaders })
+    return handleCorsPreflightRequest()
   }
   }
     const url = new URL(req.url)
     const url = new URL(req.url)
     const path = url.pathname.replace('/auth/', '')
     const path = url.pathname.replace('/auth/', '')

+ 4 - 5
supabase/functions/complete-shoprenter-install/index.ts

@@ -3,10 +3,9 @@ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
 import { wrapHandler } from '../_shared/error-handler.ts'
 import { wrapHandler } from '../_shared/error-handler.ts'
 import { createScraperClient } from '../_shared/scraper-client.ts'
 import { createScraperClient } from '../_shared/scraper-client.ts'
 
 
-const corsHeaders = {
-  'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
-}
+import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
+
+const corsHeaders = getCorsHeaders();
 
 
 // Register store with scraper service
 // Register store with scraper service
 async function registerStoreWithScraper(
 async function registerStoreWithScraper(
@@ -64,7 +63,7 @@ async function registerStoreWithScraper(
 
 
 serve(wrapHandler('complete-shoprenter-install', async (req) => {
 serve(wrapHandler('complete-shoprenter-install', async (req) => {
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
-    return new Response('ok', { headers: corsHeaders })
+    return handleCorsPreflightRequest()
   }
   }
 
 
   try {
   try {

+ 3 - 5
supabase/functions/custom-content-create/index.ts

@@ -17,16 +17,14 @@ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
 import { chunkText } from '../_shared/text-chunker.ts';
 import { chunkText } from '../_shared/text-chunker.ts';
 import { syncCustomContent } from '../_shared/qdrant-client.ts';
 import { syncCustomContent } from '../_shared/qdrant-client.ts';
 import { validateContent } from '../_shared/content-validator.ts';
 import { validateContent } from '../_shared/content-validator.ts';
+import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
 
 
-const corsHeaders = {
-  'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
-};
+const corsHeaders = getCorsHeaders();
 
 
 Deno.serve(async (req) => {
 Deno.serve(async (req) => {
   // Handle CORS preflight
   // Handle CORS preflight
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
-    return new Response(null, { headers: corsHeaders });
+    return handleCorsPreflightRequest();
   }
   }
 
 
   try {
   try {

+ 3 - 6
supabase/functions/custom-content-delete/index.ts

@@ -13,17 +13,14 @@
 
 
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
 import { deleteCustomContent } from '../_shared/qdrant-client.ts';
 import { deleteCustomContent } from '../_shared/qdrant-client.ts';
+import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
 
 
-const corsHeaders = {
-  'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
-  'Access-Control-Allow-Methods': 'DELETE, OPTIONS',
-};
+const corsHeaders = getCorsHeaders(['DELETE']);
 
 
 Deno.serve(async (req) => {
 Deno.serve(async (req) => {
   // Handle CORS preflight
   // Handle CORS preflight
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
-    return new Response(null, { headers: corsHeaders });
+    return handleCorsPreflightRequest(['DELETE']);
   }
   }
 
 
   try {
   try {

+ 3 - 5
supabase/functions/custom-content-list/index.ts

@@ -7,16 +7,14 @@
  */
  */
 
 
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
+import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
 
 
-const corsHeaders = {
-  'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
-};
+const corsHeaders = getCorsHeaders();
 
 
 Deno.serve(async (req) => {
 Deno.serve(async (req) => {
   // Handle CORS preflight
   // Handle CORS preflight
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
-    return new Response(null, { headers: corsHeaders });
+    return handleCorsPreflightRequest();
   }
   }
 
 
   try {
   try {

+ 4 - 5
supabase/functions/custom-content-retry/index.ts

@@ -9,15 +9,14 @@
 
 
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
 
 
-const corsHeaders = {
-  'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
-};
+import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
+
+const corsHeaders = getCorsHeaders();
 
 
 Deno.serve(async (req) => {
 Deno.serve(async (req) => {
   // Handle CORS preflight
   // Handle CORS preflight
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
-    return new Response(null, { headers: corsHeaders });
+    return handleCorsPreflightRequest();
   }
   }
 
 
   try {
   try {

+ 4 - 5
supabase/functions/custom-content-sync-status/index.ts

@@ -9,15 +9,14 @@
 
 
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
 
 
-const corsHeaders = {
-  'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
-};
+import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
+
+const corsHeaders = getCorsHeaders();
 
 
 Deno.serve(async (req) => {
 Deno.serve(async (req) => {
   // Handle CORS preflight
   // Handle CORS preflight
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
-    return new Response(null, { headers: corsHeaders });
+    return handleCorsPreflightRequest();
   }
   }
 
 
   try {
   try {

+ 4 - 5
supabase/functions/custom-content-upload/index.ts

@@ -20,10 +20,9 @@
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
 import { calculateChecksum } from '../_shared/pdf-processor.ts';
 import { calculateChecksum } from '../_shared/pdf-processor.ts';
 
 
-const corsHeaders = {
-  'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
-};
+import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
+
+const corsHeaders = getCorsHeaders();
 
 
 interface UploadRequest {
 interface UploadRequest {
   file: File;
   file: File;
@@ -34,7 +33,7 @@ interface UploadRequest {
 Deno.serve(async (req) => {
 Deno.serve(async (req) => {
   // Handle CORS preflight
   // Handle CORS preflight
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
-    return new Response(null, { headers: corsHeaders });
+    return handleCorsPreflightRequest();
   }
   }
 
 
   try {
   try {

+ 4 - 5
supabase/functions/custom-content-view/index.ts

@@ -10,15 +10,14 @@
 
 
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
 
 
-const corsHeaders = {
-  'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
-};
+import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
+
+const corsHeaders = getCorsHeaders();
 
 
 Deno.serve(async (req) => {
 Deno.serve(async (req) => {
   // Handle CORS preflight
   // Handle CORS preflight
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
-    return new Response(null, { headers: corsHeaders });
+    return handleCorsPreflightRequest();
   }
   }
 
 
   try {
   try {

+ 4 - 6
supabase/functions/scraper-management/index.ts

@@ -12,11 +12,9 @@
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
 import { createScraperClient, validateSameDomain } from '../_shared/scraper-client.ts';
 import { createScraperClient, validateSameDomain } from '../_shared/scraper-client.ts';
 
 
-const corsHeaders = {
-  'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
-  'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS',
-};
+import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
+
+const corsHeaders = getCorsHeaders();
 
 
 interface StoreRow {
 interface StoreRow {
   id: string;
   id: string;
@@ -67,7 +65,7 @@ async function updateStoreScraperStatus(
 Deno.serve(async (req) => {
 Deno.serve(async (req) => {
   // Handle CORS preflight requests
   // Handle CORS preflight requests
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
-    return new Response(null, { headers: corsHeaders });
+    return handleCorsPreflightRequest();
   }
   }
 
 
   try {
   try {

+ 4 - 5
supabase/functions/validate-shoprenter-hmac/index.ts

@@ -25,10 +25,9 @@ async function calculateHmacSha256(secret: string, message: string): Promise<str
   return bufferToHex(signature);
   return bufferToHex(signature);
 }
 }
 
 
-const corsHeaders = {
-  'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
-}
+import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
+
+const corsHeaders = getCorsHeaders();
 
 
 // Validate HMAC signature from ShopRenter
 // Validate HMAC signature from ShopRenter
 // Per ShopRenter documentation, HMAC is calculated from the query string without HMAC parameter
 // Per ShopRenter documentation, HMAC is calculated from the query string without HMAC parameter
@@ -80,7 +79,7 @@ async function validateHMAC(params: Record<string, string>, clientSecret: string
 
 
 serve(wrapHandler('validate-shoprenter-hmac', async (req) => {
 serve(wrapHandler('validate-shoprenter-hmac', async (req) => {
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
-    return new Response('ok', { headers: corsHeaders })
+    return handleCorsPreflightRequest()
   }
   }
 
 
   try {
   try {