gpt-engineer-app[bot] 3 dagar sedan
förälder
incheckning
020b9ed452

+ 149 - 0
supabase/functions/_shared/security.ts

@@ -0,0 +1,149 @@
+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
+ */
+export function validateCronSecret(req: Request): boolean {
+  const cronSecret = req.headers.get('x-cron-secret');
+  const expectedSecret = Deno.env.get('CRON_SECRET');
+  
+  // If no cron secret is configured, allow internal calls
+  // This maintains backward compatibility while allowing secure setup
+  if (!expectedSecret) {
+    return true;
+  }
+  
+  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;
+}

+ 78 - 5
supabase/functions/fetch-rss/index.ts

@@ -1,6 +1,6 @@
-
 import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
+import { isValidExternalUrl, verifyAuth, isInternalCall, validateCronSecret } from '../_shared/security.ts'
 
 interface RSSItem {
   title: string;
@@ -20,7 +20,7 @@ interface RSSFeed {
 
 const corsHeaders = {
   'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
+  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-cron-secret',
 }
 
 // Helper function to extract text content from XML tags
@@ -72,6 +72,19 @@ serve(async (req) => {
   }
 
   try {
+    // Authentication: Allow internal calls (cron jobs) OR authenticated users
+    const isCronJob = validateCronSecret(req);
+    const isInternal = isInternalCall(req);
+    const auth = await verifyAuth(req);
+    
+    if (!isCronJob && !isInternal && !auth) {
+      console.log('Unauthorized access attempt to fetch-rss');
+      return new Response(
+        JSON.stringify({ error: 'Unauthorized', success: false }),
+        { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
     const supabaseClient = createClient(
       Deno.env.get('SUPABASE_URL') ?? '',
       Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
@@ -79,15 +92,75 @@ serve(async (req) => {
 
     const { feedId, feedUrl } = await req.json()
 
+    // Input validation
+    if (!feedId || typeof feedId !== 'string') {
+      return new Response(
+        JSON.stringify({ error: 'Invalid feed ID', success: false }),
+        { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    if (!feedUrl || typeof feedUrl !== 'string') {
+      return new Response(
+        JSON.stringify({ error: 'Invalid feed URL', success: false }),
+        { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // SSRF Protection: Validate URL
+    const urlValidation = isValidExternalUrl(feedUrl);
+    if (!urlValidation.valid) {
+      console.log(`SSRF blocked: ${feedUrl} - ${urlValidation.error}`);
+      return new Response(
+        JSON.stringify({ error: urlValidation.error, success: false }),
+        { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // Verify the feed exists in the database (prevents arbitrary URL fetching)
+    const { data: feedData, error: feedError } = await supabaseClient
+      .from('feeds')
+      .select('id, url')
+      .eq('id', feedId)
+      .single()
+
+    if (feedError || !feedData) {
+      console.log(`Feed not found: ${feedId}`);
+      return new Response(
+        JSON.stringify({ error: 'Feed not found', success: false }),
+        { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
     console.log(`Fetching RSS for feed: ${feedId}, URL: ${feedUrl}`)
 
-    // Fetch RSS content
-    const response = await fetch(feedUrl)
+    // Fetch RSS content with timeout
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
+    
+    let response;
+    try {
+      response = await fetch(feedUrl, { 
+        signal: controller.signal,
+        headers: {
+          'User-Agent': 'Mozilla/5.0 (compatible; RSS Feed Reader/1.0)'
+        }
+      });
+    } finally {
+      clearTimeout(timeoutId);
+    }
+
     if (!response.ok) {
       throw new Error(`Failed to fetch RSS: ${response.statusText}`)
     }
 
     const rssText = await response.text()
+    
+    // Limit content size to prevent memory exhaustion
+    if (rssText.length > 5 * 1024 * 1024) { // 5MB limit
+      throw new Error('RSS feed content too large');
+    }
+    
     console.log(`RSS content length: ${rssText.length}`)
     
     // Parse RSS using regex patterns
@@ -240,7 +313,7 @@ serve(async (req) => {
     console.error('Error in fetch-rss function:', error)
     return new Response(
       JSON.stringify({ 
-        error: error?.message || 'Unknown error',
+        error: 'An error occurred while fetching the feed',
         success: false 
       }),
       { 

+ 71 - 18
supabase/functions/fetch-website-rss/index.ts

@@ -1,4 +1,5 @@
 import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
+import { isValidExternalUrl, verifyAuth } from '../_shared/security.ts';
 
 const corsHeaders = {
   'Access-Control-Allow-Origin': '*',
@@ -18,29 +19,64 @@ serve(async (req) => {
   }
 
   try {
+    // Authentication: Require authenticated user
+    const auth = await verifyAuth(req);
+    if (!auth) {
+      console.log('Unauthorized access attempt to fetch-website-rss');
+      return new Response(
+        JSON.stringify({ success: false, error: 'Unauthorized' }),
+        { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      );
+    }
+
     const { url } = await req.json();
 
-    if (!url) {
+    if (!url || typeof url !== 'string') {
+      return new Response(
+        JSON.stringify({ success: false, error: 'Valid URL is required' }),
+        { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      );
+    }
+
+    // SSRF Protection: Validate URL
+    const urlValidation = isValidExternalUrl(url);
+    if (!urlValidation.valid) {
+      console.log(`SSRF blocked in fetch-website-rss: ${url} - ${urlValidation.error}`);
       return new Response(
-        JSON.stringify({ error: 'URL is required' }),
+        JSON.stringify({ success: false, error: urlValidation.error }),
         { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
       );
     }
 
     console.log('Fetching website RSS for URL:', url);
 
-    // Fetch the webpage
-    const response = await fetch(url, {
-      headers: {
-        'User-Agent': 'Mozilla/5.0 (compatible; RSS Feed Detector/1.0)',
-      },
-    });
+    // Fetch the webpage with timeout
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
+
+    let response;
+    try {
+      response = await fetch(url, {
+        signal: controller.signal,
+        headers: {
+          'User-Agent': 'Mozilla/5.0 (compatible; RSS Feed Detector/1.0)',
+        },
+      });
+    } finally {
+      clearTimeout(timeoutId);
+    }
 
     if (!response.ok) {
       throw new Error(`Failed to fetch website: ${response.status}`);
     }
 
     const html = await response.text();
+    
+    // Limit content size
+    if (html.length > 5 * 1024 * 1024) { // 5MB limit
+      throw new Error('Website content too large');
+    }
+    
     const feeds: RSSFeed[] = [];
 
     // Extract site name from <title> tag
@@ -54,12 +90,12 @@ serve(async (req) => {
     for (const link of links) {
       const typeMatch = link.match(/type=["']([^"']+)["']/i);
       const hrefMatch = link.match(/href=["']([^"']+)["']/i);
-      const titleMatch = link.match(/title=["']([^"']+)["']/i);
+      const linkTitleMatch = link.match(/title=["']([^"']+)["']/i);
 
       if (typeMatch && hrefMatch) {
         const type = typeMatch[1];
         const href = hrefMatch[1];
-        const title = titleMatch ? titleMatch[1] : 'RSS Feed';
+        const title = linkTitleMatch ? linkTitleMatch[1] : 'RSS Feed';
 
         if (
           type.includes('application/rss+xml') ||
@@ -69,11 +105,15 @@ serve(async (req) => {
           // Convert relative URLs to absolute
           const feedUrl = href.startsWith('http') ? href : new URL(href, url).toString();
           
-          feeds.push({
-            url: feedUrl,
-            title,
-            type,
-          });
+          // Validate the discovered feed URL too
+          const feedUrlValidation = isValidExternalUrl(feedUrl);
+          if (feedUrlValidation.valid) {
+            feeds.push({
+              url: feedUrl,
+              title,
+              type,
+            });
+          }
         }
       }
     }
@@ -85,8 +125,21 @@ serve(async (req) => {
       
       for (const path of commonPaths) {
         const testUrl = `${baseUrl.origin}${path}`;
+        
+        // Validate fallback URL
+        const testUrlValidation = isValidExternalUrl(testUrl);
+        if (!testUrlValidation.valid) continue;
+        
         try {
-          const testResponse = await fetch(testUrl, { method: 'HEAD' });
+          const testController = new AbortController();
+          const testTimeoutId = setTimeout(() => testController.abort(), 5000); // 5s timeout for fallback checks
+          
+          const testResponse = await fetch(testUrl, { 
+            method: 'HEAD',
+            signal: testController.signal
+          });
+          clearTimeout(testTimeoutId);
+          
           if (testResponse.ok) {
             feeds.push({
               url: testUrl,
@@ -101,7 +154,7 @@ serve(async (req) => {
       }
     }
 
-    console.log('Found feeds:', feeds);
+    console.log('Found feeds:', feeds.length);
 
     if (feeds.length === 0) {
       return new Response(
@@ -130,7 +183,7 @@ serve(async (req) => {
     return new Response(
       JSON.stringify({ 
         success: false,
-        error: (error as Error).message || 'Failed to detect RSS feed' 
+        error: 'Failed to detect RSS feed' 
       }),
       { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
     );

+ 74 - 9
supabase/functions/fetch-youtube-rss/index.ts

@@ -1,4 +1,5 @@
 import { serve } from "https://deno.land/std@0.177.0/http/server.ts"
+import { isValidExternalUrl, verifyAuth } from '../_shared/security.ts'
 
 const corsHeaders = {
   'Access-Control-Allow-Origin': '*',
@@ -11,9 +12,34 @@ serve(async (req) => {
   }
 
   try {
+    // Authentication: Require authenticated user
+    const auth = await verifyAuth(req);
+    if (!auth) {
+      console.log('Unauthorized access attempt to fetch-youtube-rss');
+      return new Response(
+        JSON.stringify({ error: 'Unauthorized' }),
+        { 
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+          status: 401
+        }
+      )
+    }
+
     const { url } = await req.json()
     
-    if (!url || !url.includes('youtube.com')) {
+    // Input validation - must be a YouTube URL
+    if (!url || typeof url !== 'string') {
+      return new Response(
+        JSON.stringify({ error: 'Valid URL is required' }),
+        { 
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+          status: 400
+        }
+      )
+    }
+
+    // Validate it's a YouTube URL specifically
+    if (!url.includes('youtube.com') && !url.includes('youtu.be')) {
       return new Response(
         JSON.stringify({ error: 'Invalid YouTube URL' }),
         { 
@@ -23,14 +49,36 @@ serve(async (req) => {
       )
     }
 
+    // SSRF Protection: Validate URL format
+    const urlValidation = isValidExternalUrl(url);
+    if (!urlValidation.valid) {
+      console.log(`SSRF blocked in fetch-youtube-rss: ${url} - ${urlValidation.error}`);
+      return new Response(
+        JSON.stringify({ error: urlValidation.error }),
+        { 
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+          status: 400
+        }
+      )
+    }
+
     console.log('Fetching YouTube page:', url)
     
-    // Fetch the YouTube page
-    const response = await fetch(url, {
-      headers: {
-        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
-      }
-    })
+    // Fetch the YouTube page with timeout
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
+    
+    let response;
+    try {
+      response = await fetch(url, {
+        signal: controller.signal,
+        headers: {
+          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
+        }
+      })
+    } finally {
+      clearTimeout(timeoutId);
+    }
     
     if (!response.ok) {
       throw new Error(`Failed to fetch page: ${response.status}`)
@@ -38,6 +86,11 @@ serve(async (req) => {
     
     const html = await response.text()
     
+    // Limit content size
+    if (html.length > 5 * 1024 * 1024) { // 5MB limit
+      throw new Error('Page content too large');
+    }
+    
     // Look for the RSS feed link in the HTML
     const rssLinkMatch = html.match(/<link[^>]+rel="alternate"[^>]+type="application\/rss\+xml"[^>]+href="([^"]+)"/i)
     
@@ -53,6 +106,18 @@ serve(async (req) => {
     
     const rssUrl = rssLinkMatch[1]
     
+    // Validate the discovered RSS URL
+    const rssUrlValidation = isValidExternalUrl(rssUrl);
+    if (!rssUrlValidation.valid) {
+      return new Response(
+        JSON.stringify({ error: 'Invalid RSS feed URL discovered' }),
+        { 
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+          status: 400
+        }
+      )
+    }
+    
     // Also try to extract the channel name from the page title
     const titleMatch = html.match(/<title>([^<]+)<\/title>/i)
     let channelName = null
@@ -78,11 +143,11 @@ serve(async (req) => {
   } catch (error: any) {
     console.error('Error:', error)
     return new Response(
-      JSON.stringify({ error: error?.message || 'Unknown error' }),
+      JSON.stringify({ error: 'Failed to fetch YouTube RSS feed' }),
       { 
         headers: { ...corsHeaders, 'Content-Type': 'application/json' },
         status: 500
       }
     )
   }
-})
+})

+ 26 - 5
supabase/functions/purge-articles/index.ts

@@ -1,16 +1,18 @@
 /**
  * Purge Articles Edge Function
- * Version: 2.0
- * Last updated: 2025-01-20
+ * Version: 2.1
+ * Last updated: 2026-01-13
  * Purpose: Automatically purge old articles and send email reports to admins
+ * Security: Requires super user authentication or cron secret
  */
 
 import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
+import { verifySuperUser, validateCronSecret, isInternalCall } from '../_shared/security.ts';
 
 const corsHeaders = {
   'Access-Control-Allow-Origin': '*',
-  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
+  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, x-cron-secret',
 };
 
 serve(async (req) => {
@@ -23,6 +25,25 @@ serve(async (req) => {
   }
 
   try {
+    // Authentication: Allow cron jobs, internal calls, or super users
+    const isCronJob = validateCronSecret(req);
+    const isInternal = isInternalCall(req);
+    const isSuperUser = await verifySuperUser(req);
+    
+    if (!isCronJob && !isInternal && !isSuperUser) {
+      console.log('Unauthorized access attempt to purge-articles');
+      return new Response(
+        JSON.stringify({
+          success: false,
+          error: 'Unauthorized - Super user access or cron secret required'
+        }),
+        {
+          status: 401,
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+        }
+      );
+    }
+
     console.log('🗑️ Starting automatic article purge...');
 
     // Create Supabase client
@@ -45,7 +66,7 @@ serve(async (req) => {
     const adminEmails = result.admin_emails;
 
     console.log(`Deleted ${deletedCount} articles`);
-    console.log(`Admin emails:`, adminEmails);
+    console.log(`Admin emails count:`, adminEmails?.length || 0);
 
     // Send email report to admins
     if (adminEmails && adminEmails.length > 0) {
@@ -83,7 +104,7 @@ serve(async (req) => {
     return new Response(
       JSON.stringify({
         success: false,
-        error: error?.message || 'Unknown error'
+        error: 'An error occurred during the purge operation'
       }),
       {
         status: 500,

+ 67 - 10
supabase/functions/update-feed/index.ts

@@ -1,6 +1,6 @@
-
 import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
+import { isValidExternalUrl, verifySuperUser } from '../_shared/security.ts'
 
 const corsHeaders = {
   'Access-Control-Allow-Origin': '*',
@@ -13,11 +13,39 @@ serve(async (req) => {
   }
 
   try {
+    // Authentication: Only super users can update feeds
+    const isSuperUser = await verifySuperUser(req);
+    if (!isSuperUser) {
+      console.log('Unauthorized access attempt to update-feed');
+      return new Response(
+        JSON.stringify({ error: 'Unauthorized - Super user access required' }),
+        { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
     const { feedId, url } = await req.json()
 
-    if (!feedId || !url) {
+    // Input validation
+    if (!feedId || typeof feedId !== 'string') {
+      return new Response(
+        JSON.stringify({ error: 'Valid Feed ID is required' }),
+        { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    if (!url || typeof url !== 'string') {
+      return new Response(
+        JSON.stringify({ error: 'Valid URL is required' }),
+        { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // SSRF Protection: Validate URL
+    const urlValidation = isValidExternalUrl(url);
+    if (!urlValidation.valid) {
+      console.log(`SSRF blocked in update-feed: ${url} - ${urlValidation.error}`);
       return new Response(
-        JSON.stringify({ error: 'Feed ID and URL are required' }),
+        JSON.stringify({ error: urlValidation.error }),
         { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
       )
     }
@@ -28,20 +56,49 @@ serve(async (req) => {
       Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
     )
 
+    // Verify the feed exists
+    const { data: existingFeed, error: feedCheckError } = await supabase
+      .from('feeds')
+      .select('id')
+      .eq('id', feedId)
+      .single()
+
+    if (feedCheckError || !existingFeed) {
+      return new Response(
+        JSON.stringify({ error: 'Feed not found' }),
+        { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
     console.log(`Fetching RSS feed from: ${url}`)
 
-    // Fetch RSS feed
-    const response = await fetch(url, {
-      headers: {
-        'User-Agent': 'Mozilla/5.0 (compatible; RSS Feed Updater/1.0)'
-      }
-    })
+    // Fetch RSS feed with timeout
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
+
+    let response;
+    try {
+      response = await fetch(url, {
+        signal: controller.signal,
+        headers: {
+          'User-Agent': 'Mozilla/5.0 (compatible; RSS Feed Updater/1.0)'
+        }
+      })
+    } finally {
+      clearTimeout(timeoutId);
+    }
 
     if (!response.ok) {
       throw new Error(`HTTP error! status: ${response.status}`)
     }
 
     const rssText = await response.text()
+    
+    // Limit content size
+    if (rssText.length > 5 * 1024 * 1024) { // 5MB limit
+      throw new Error('RSS feed content too large');
+    }
+    
     console.log('RSS content fetched successfully')
 
     // Parse RSS content to extract metadata
@@ -119,7 +176,7 @@ serve(async (req) => {
   } catch (error: any) {
     console.error('Error updating feed:', error)
     return new Response(
-      JSON.stringify({ error: error?.message || 'Unknown error' }),
+      JSON.stringify({ error: 'An error occurred while updating the feed' }),
       { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
     )
   }