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.
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
https://localhost:*), ama postMessage hedefi joker kabul etmez — bu yüzden ikisi ayrı ele alınır (aşağıda 1 ve 2).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
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.
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).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.// 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
});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).openUrl action'ı ile açılıyor (window.open/target="_blank" sandbox'ta çalışmaz).