iframe Güvenliği (ZORUNLU)

Custom UI sayfaların kötü niyetli sitelerce çerçevelenip (clickjacking) tenant oturumunun istismarını engellemek için iki kural zorunludur: (1) CSP frame-ancestors ile yalnız panel origin'ine izin ver, (2) App Bridge mesajlaşmasını origin'e pinle ve gelen event.origin'i doğrula.

Panel origin'leri

Custom UI sayfanı çerçeveleyen (iframe'e gömen) Restomenum paneli iki şekilde gelebilir — hem CSP frame-ancestors'ta hem App Bridge origin doğrulamasında hepsine izin ver:

sabit origin'ler (bulut panel — DEV panel dahil)
  • https://app.restomenum.com
  • https://test-restomenu.web.app

offline / yerel kurulum (app indirilip localhost'ta çalıştırılıyor)
  • http(s)://localhost:<port>     ← port DEĞİŞKEN (Vite 5173, CRA 3000, meşgulse +1…)
  • http(s)://127.0.0.1:<port>     ← yerel sunucu genelde düz http servis eder
Yerel panelin portu önceden bilinemez. CSP bunu joker ile çözer (https://localhost:*), ama postMessage hedefi joker kabul etmez — bu yüzden ikisi ayrı ele alınır (aşağıda 1 ve 2).

1) CSP frame-ancestors (otomatik denetlenir)

Custom UI sayfanın HTTP yanıtında bu header bulunmalı. Sürüm onayında otomatik denetlenir. CSP port jokerini desteklediği için yerel panel de böyle kapsanır:

Content-Security-Policy: frame-ancestors https://app.restomenum.com https://test-restomenu.web.app http://localhost:* https://localhost:* http://127.0.0.1:* https://127.0.0.1:*
✓ GEÇER:  frame-ancestors var + panel origin'lerini içerir + '*' yok
✗ REDDET: header yok · '*' · 'none' · panel origin'i yok  → onay bloklanır

2) App Bridge origin-pinleme (manuel review)

postMessage'ı wildcard '*' ile değil panel origin'ine pinli gönder ve gelen mesajlarda event.origin'i doğrula. Bu JS içinde olduğundan reviewer manuel teyit eder.

Sabit liste ile includes() karşılaştırması YETMEZ. Yerel panelin portu önceden bilinemez ("https://localhost:*" hiçbir gerçek origin'e eşit değildir) — bu kontrol yerel kurulumda gelen tüm mesajları sessizce reddeder. Bunun yerine desen eşleştir: sabit origin'lerden biri veya http(s)://localhost|127.0.0.1:<port> (SDK: isPanelOrigin).
Üst pencerenin origin'ini document.referrer'dan tespit etme — iframe referrerPolicy="no-referrer" ile yüklenir, document.referrer boştur. Yerel panelde hedefi öğrenmenin yolu: panel kendi origin'ini iframe URL'ine koyar (?panelOrigin=… — sektör deseni: Atlassian Connect xdm_e, Shopify App Bridge host), sen doğrulayıp pinlersin (SDK: readPanelOrigin). 🔒 Doğrulamadan kullanırsan kötü niyetli bir çerçeveleyici ?panelOrigin=https://evil.com verip session token'ı kendine yönlendirir.

Referans

// Custom UI iframe'ini çerçeveleyen üst pencere (panel) iki şekilde gelebilir:
//   • sabit origin'ler : https://app.restomenum.com  ·  https://test-restomenu.web.app
//   • offline/yerel kurulum: https://localhost:<port> · https://127.0.0.1:<port>  (port DEĞİŞKEN)

// 1) Custom UI sayfan SADECE panel tarafından çerçevelenebilmeli (anti-clickjacking).
//    Sayfanın HTTP yanıtında şu header ZORUNLU (onayda otomatik denetlenir).
//    CSP port jokerini DESTEKLER → yerel panel böyle kapsanır:
Content-Security-Policy: frame-ancestors https://app.restomenum.com https://test-restomenu.web.app http://localhost:* https://localhost:* http://127.0.0.1:* https://127.0.0.1:*
//    ❌ header yok · ❌ frame-ancestors '*' · ❌ 'none' · ❌ panel origin'i yoksa  → onay reddedilir.

// 2) App Bridge postMessage'ı wildcard '*' DEĞİL, panel origin'ine PİNLİ gönder.
//    ⚠️ targetOrigin JOKER KABUL ETMEZ: postMessage(msg, 'https://localhost:*') → SyntaxError.
//       Bu yüzden yerel panelin TAM origin'i gerekir; port önceden bilinemez.
//    ⚠️ Üst pencerenin origin'i document.referrer'dan da ÖĞRENİLEMEZ (iframe no-referrer ile yüklenir).
//    ÇÖZÜM (sektör deseni — Atlassian Connect xdm_e / Shopify App Bridge host):
//       panel kendi origin'ini iframe URL'ine koyar → ?panelOrigin=https%3A%2F%2Flocalhost%3A5173
//    🔒 Gelen değeri MUTLAKA doğrula; doğrulamazsan kötü niyetli bir çerçeveleyici
//       ?panelOrigin=https://evil.com verip session token'ı kendine yönlendirir.
const PANEL_ORIGINS = ["https://app.restomenum.com","https://test-restomenu.web.app"];
const LOCAL_PANEL = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d{1,5})?$/;
const isPanelOrigin = (o) => PANEL_ORIGINS.includes(o) || LOCAL_PANEL.test(o);   // SDK: isPanelOrigin

function resolvePanelOrigin() {
  const raw = new URLSearchParams(location.search).get('panelOrigin');  // SDK: readPanelOrigin()
  if (raw && isPanelOrigin(raw)) return raw;      // ← doğrulandı, hedef olarak pinlenebilir
  return null;                                    // yoksa sabit origin'lere düş
}

const panelOrigin = resolvePanelOrigin();
const msg = { type: 'ready' };
if (panelOrigin) {
  window.parent.postMessage(msg, panelOrigin);                          // yerel VEYA sabit panel
} else {
  // Sabit origin'ler: targetOrigin eşleşmeyen gönderim TESLİM EDİLMEZ → yalnız gerçek panel alır.
  PANEL_ORIGINS.forEach((origin) => window.parent.postMessage(msg, origin));
}

window.addEventListener('message', (e) => {
  if (!isPanelOrigin(e.origin)) return;   // gelen origin doğrula (liste includes() YETMEZ — yerel port)
  // ... güvenli: e.data işle
});

Kontrol listesi

  • ✅ Sayfa yanıtında Content-Security-Policy: frame-ancestors https://app.restomenum.com https://test-restomenu.web.app http://localhost:* https://localhost:* http://127.0.0.1:* https://127.0.0.1:* — sabit origin'ler + yerel panel jokerleri var.
  • ✅ frame-ancestors '*' veya 'none' içermiyor.
  • ✅ postMessage(data, panelOrigin) — hedef TAM bir origin'e pinli (doğrulanmış ?panelOrigin= ya da sabit liste); wildcard yok, document.referrer'a bağlı değil.
  • ✅ if (!isPanelOrigin(event.origin)) return; — gelen origin desenle doğrulanıyor (yerel panel portu değişken; düz includes() onu reddeder).
  • ✅ Session token backend'de doğrulanıyor.
  • ✅ Dış linkler openUrl action'ı ile açılıyor (window.open/target="_blank" sandbox'ta çalışmaz).
Bu kurallara uymayan Custom UI sayfaları içeren sürümler onaydan geçemez. Detaylı mekanizma için Custom UI Sayfaları.