CORS Security

CORS (Cross-Origin Resource Sharing) הוא מנגנון אבטחה המיושם על ידי דפדפנים מודרניים השולט באופן שבו יישומי אינטרנט יכולים לבצע בקשות HTTP לדומיינים השונים מזה ששירת את הדף המקורי, תוך הרפיה מבוקרת של ה-Same-Origin Policy (SOP) - מדיניות אבטחה בסיסית המגבילה סקריפטים ממקור אחד מלגשת למשאבים של מקור אחר. אף שה-CORS חיוני לארכיטקטורות מודרניות של יישומי אינטרנט, שבהן צד-לקוח (front-end) נדרש לעיתים קרובות לצרוך ממשקי API המתארחים בדומיינים שונים, תצורתו הלקויה מהווה אחת מהפגיעויות הנפוצות והמסוכנות ביותר ביישומי אינטרנט עכשוויים. שגיאות תצורת CORS עלולות לחשוף נתונים רגישים לדומיינים לא מורשים, לאפשר התקפות Cross-Site Request Forgery (CSRF) אפילו בנוכחות אסימוני אנטי-CSRF, להקל על גניבת אישורי גישה, ובמקרים קיצוניים, לאפשר לתוקפים לבצע פעולות בעלות הרשאות בשם משתמשים מאומתים. הבעיה מחמירה בשל העובדה שמפתחים רבים, בנתקלם בשגיאות CORS במהלך הפיתוח, בוחרים בפתרונות מתירניים מדי (כגון שימוש בתו הכללי "*" או שיקוף אוטומטי של מקור הבקשה) בלי להבין במלואן את ההשלכות האבטחתיות. מאמר זה בוחן לעומק את יסודות ה-CORS, פגיעויות תצורה נפוצות, ומבסס שיטות עבודה איתנות ליישום מאובטח בפלטפורמות ובמסגרות עבודה שונות, תוך איזון בין פונקציונליות לבין עמדה הגנתית הולמת.

Same-Origin Policy (SOP)

דפדפנים מיישמים SOP: סקריפטים יכולים לגשת רק למשאבים מאותו מקור (פרוטוקול + דומיין + פורט). CORS מרפה את ה-SOP באופן מבוקר.

      # אותו מקור
      https://example.com/api ← https://example.com/app [OK]
      # מקורות שונים (חסום על ידי SOP)
      https://example.com ← http://example.com (פרוטוקול)
      https://example.com ← https://api.example.com (תת-דומיין)
      https://example.com ← https://example.com:8080 (פורט)
      

CORS Headers

Access-Control-Allow-Origin

      # התרת מקור מסוים (מומלץ)
      Access-Control-Allow-Origin: https://trusted.com
      # התרת כל מקור (מסוכן!)
      Access-Control-Allow-Origin: *
      # דינמי המבוסס על whitelist (נכון)
      const allowedOrigins = ['https://app1.com', 'https://app2.com'];
      const origin = request.headers.origin;
      if (allowedOrigins.includes(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      }
      

Headers חשובים נוספים

      # התרת אישורי גישה (cookies, auth headers)
      Access-Control-Allow-Credentials: true
      # שיטות HTTP מותרות
      Access-Control-Allow-Methods: GET, POST, PUT, DELETE
      # Headers מותרים בבקשות
      Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
      # Headers החשופים ל-JavaScript בצד הלקוח
      Access-Control-Expose-Headers: X-Custom-Header, X-Request-Id
      # זמן המטמון של ה-preflight (שניות)
      Access-Control-Max-Age: 86400
      

Preflight Requests

דפדפנים שולחים בקשת OPTIONS לפני בקשות "לא פשוטות" כדי לאמת הרשאות.

      # הלקוח שולח preflight
      OPTIONS /api/resource HTTP/1.1
      Origin: https://app.com
      Access-Control-Request-Method: DELETE
      Access-Control-Request-Headers: Authorization
      # השרת מגיב עם הרשאות
      HTTP/1.1 204 No Content
      Access-Control-Allow-Origin: https://app.com
      Access-Control-Allow-Methods: GET, POST, DELETE
      Access-Control-Allow-Headers: Authorization
      Access-Control-Max-Age: 86400
      

פגיעויות CORS נפוצות

1. תו כללי עם אישורי גישה

      # [שגיאה] פגיע - אינו עובד ומסוכן
      Access-Control-Allow-Origin: *
      Access-Control-Allow-Credentials: true
      # דפדפנים חוסמים שילוב זה
      # [OK] נכון - מקור מסוים עם אישורי גישה
      Access-Control-Allow-Origin: https://trusted.com
      Access-Control-Allow-Credentials: true
      

2. Reflection Attack

      # [שגיאה] פגיע - משקף כל origin
      const origin = request.headers.origin;
      res.setHeader('Access-Control-Allow-Origin', origin);
      res.setHeader('Access-Control-Allow-Credentials', 'true');
      # [OK] נכון - אימות whitelist
      const allowedOrigins = ['https://app.com', 'https://admin.com'];
      const origin = request.headers.origin;
      if (allowedOrigins.includes(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      res.setHeader('Access-Control-Allow-Credentials', 'true');
      }
      

3. Subdomain Wildcard

      # [שגיאה] פגיע - regex מיושם בצורה לקויה
      const origin = request.headers.origin;
      if (/https:\/\/.*\.example\.com/.test(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      }
      // מקבל https://evil.example.com.attacker.com
      # [OK] נכון - אימות קפדני
      const origin = request.headers.origin;
      if (/^https:\/\/[a-z0-9-]+\.example\.com$/.test(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      }
      

תצורה מאובטחת לפי טכנולוגיה

Node.js/Express (CORS middleware)

      const cors = require('cors');
      // תצורה מאובטחת
      const corsOptions = {
      origin: function (origin, callback) {
      const allowedOrigins = [
      'https://app.example.com',
      'https://admin.example.com'
      ];
      if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
      } else {
      callback(new Error('Not allowed by CORS'));
      }
      },
      credentials: true,
      methods: ['GET', 'POST', 'PUT', 'DELETE'],
      allowedHeaders: ['Content-Type', 'Authorization'],
      maxAge: 86400
      };
      app.use(cors(corsOptions));
      

Nginx

      # תצורה מותנית
      map $http_origin $cors_origin {
      default "";
      "~^https://app\\.example\\.com$" $http_origin;
      "~^https://admin\\.example\\.com$" $http_origin;
      }
      server {
      location /api {
      if ($cors_origin != "") {
      add_header Access-Control-Allow-Origin $cors_origin always;
      add_header Access-Control-Allow-Credentials true always;
      add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
      add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
      }
      if ($request_method = OPTIONS) {
      return 204;
      }
      }
      }
      

Apache

      # .htaccess
      SetEnvIf Origin "^https://(app|admin)\\.example\\.com$" CORS_ORIGIN=$0
      Header always set Access-Control-Allow-Origin "%e" env=CORS_ORIGIN
      Header always set Access-Control-Allow-Credentials "true" env=CORS_ORIGIN
      Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE" env=CORS_ORIGIN
      Header always set Access-Control-Allow-Headers "Authorization, Content-Type" env=CORS_ORIGIN
      # מענה ל-preflight OPTIONS
      RewriteEngine On
      RewriteCond % OPTIONS
      RewriteRule ^(.*)$ $1 [R=204,L]
      

Testing CORS

      # בדיקה עם curl
      curl -H "Origin: https://evil.com" \\
      -H "Access-Control-Request-Method: DELETE" \\
      -H "Access-Control-Request-Headers: Authorization" \\
      -X OPTIONS \\
      https://api.example.com/resource
      # בדיקת JavaScript
      fetch('https://api.example.com/data', {
      method: 'GET',
      credentials: 'include',
      headers: {
      'Content-Type': 'application/json'
      }
      }).then(response => console.log(response));
      

Best Practices

  • לעולם אל תשתמשו בתו הכללי (*) בממשקי API עם נתונים רגישים
  • Whitelist מפורשת של מקורות מותרים
  • אימות קפדני של המקור עם regex מאובטח
  • צמצמו credentials: הפעילו רק אם זה באמת נחוץ
  • Least privilege: התירו רק את השיטות וה-headers הנחוצים
  • Cache preflight: השתמשו ב-Max-Age כדי לצמצם את התקורה
  • Monitoring: תעדו דחיות CORS חשודות

רשימת תיוג ל-CORS מאובטח

  • [OK] המקור אומת מול whitelist מפורשת
  • [OK] ה-regex של האימות אינו מאפשר עקיפות
  • [OK] Credentials מופעלים רק בעת הצורך
  • [OK] השיטות וה-headers הוגבלו למינימום
  • [OK] ה-preflight הוגדר כראוי
  • [OK] נבדק מול מקורות זדוניים
  • [OK] לוגים של בקשות שנדחו מנוטרים