Skip to content

Frame-buster e fallback anti-loop

Problema

La route r.$path.tsx rende l'ERP PHP dentro un iframe. Se per qualche ragione l'ERP risponde con una redirect verso la homepage (sessione scaduta, cookie non inviati, ecc.) il controller Home::_load_home() serve index.html della React app. Senza protezioni la React app si monta dentro l'iframe, ricarica r.$path.tsx, crea un nuovo iframe verso l'ERP e il ciclo si ripete fino a esaurire memoria: ogni livello mostra l'intera applicazione, topbar compresa.

Lo scenario tipico in cui si manifesta e' dopo un login con loginData.redirect = 'password_change': la pagina di cambio password forzato viene aperta dentro un iframe, l'ERP non ha sessione valida per quell'iframe e la redirect innesca il loop.

Soluzione in due livelli

1. Frame-buster in app/entry.client.tsx

Prima di chiamare hydrateRoot, il codice controlla se la pagina sta girando dentro un iframe e, in quel caso, sostituisce la top window con l'URL corrente. Il browser ricarica la pagina come top-level, l'iframe parent scompare e non c'e' annidamento.

ts
if (isInIframe() && !isDevRoute() && !hasSkipBreakoutFlag()) {
  window.top.location.href = window.location.href;
  // hydrateRoot viene saltato: la pagina sta per essere rimpiazzata
}

React app ed ERP sono sempre serviti dalla stessa origin, quindi il break-out riesce senza problemi di same-origin policy.

2. Safety net in app/routes/r.$path.tsx

Se per qualsiasi motivo il break-out non parte (bug, flag dev, estensioni browser che intercettano window.top) il default export di r.$path.tsx controlla isInIframe() e mostra la Card "Contesto non supportato" invece di renderizzare l'iframe ERP.

E' una rete di sicurezza: l'utente arriva in un punto morto con un bottone "Torna alla home" che riporta alla top window.

Eccezione /dev/*

Le pagine sotto /dev/* usano iframe per testare comunicazione, reload, loop e altri scenari. Il frame-buster e' whitelistato per quel prefisso tramite isDevRoute() (vedi app/lib/routing.ts): in quelle route l'iframe e' ammesso e non viene fatto break-out.

Flag di debug ?noBreakout=1

Solo in development, aggiungere ?noBreakout=1 alla URL dell'iframe disabilita il frame-buster. Serve a esporre la UI di fallback di r.$path.tsx senza dover orchestrare scenari particolari. E' ignorato in produzione (import.meta.env.PROD === true).

Pagina di test /dev/iframe-loop-test

Il file app/routes/dev/iframe-loop-test.tsx espone tre scenari:

ScenarioComportamento atteso con fix attivo
1. Auto-nested (?level=N)Mostra il loop visivo fino a MAX_LEVEL. L'annidamento funziona perche' /dev/* e' whitelistato. Serve come repro visivo del bug originale.
2. Route reale /r/admin/account/edit_account/2Avvio on-click. L'iframe carica la React app, il frame-buster sostituisce la top window con la route cambio password. Nessun annidamento.
3. Idem con ?noBreakout=1Frame-buster disabilitato: la React app idrata dentro l'iframe, r.$path.tsx rileva il contesto e mostra la Card "Contesto non supportato".

La pagina non muta lo store di autenticazione; c'e' un bottone "Reset auth-storage e torna al login" per uscire da stati inconsistenti.

Utility correlate

FileExportUso
app/lib/iframe.tsisInIframe()Boolean: true se window.self !== window.top o se l'accesso a window.top lancia.
app/lib/routing.tsgetRelativePathname(pathname?)Rimuove il prefisso BASE_URL dal pathname (gestisce /client/ in dev, / in build).
app/lib/routing.tsisDevRoute(pathname?)True se il path relativo inizia con /dev/.

Deploy

Le pagine di test sotto /dev/* girano solo in modalita' non-production (il clientLoader di app/routes/dev/layout.tsx ritorna 404 quando import.meta.env.MODE === 'production'). La whitelist del frame-buster invece resta attiva: e' un check a runtime non legato al build mode, quindi gli iframe dei test funzionano anche nelle build staging.

Sul pipeline di deploy (.github/workflows/deploy.yml) i file statici presenti in public/ devono essere inclusi esplicitamente nell'rsync: in caso contrario Apache fallback su index.php e CodeIgniter serve index.html, riproducendo un mini-loop iframe dentro la pagina di test.

Documentazione Elerama Frontend