COOP és COEP: a két fejléc, ami miatt eltűnhet a bejelentkeztető popup ablak

A SharedArrayBuffer visszatért, de csak cross-origin isolated oldalakon fut. A COOP és a COEP fejléc pontosan ezt adja meg – cserébe elszakíthatja a window.opener kapcsolatot és blokkolhatja a be nem jelentkezett cross-origin erőforrásokat.

Beteszed az ffmpeg.wasm-et vagy egy sql.js-alapú in-browser adatbázist, és az első hívásnál elszáll: SharedArrayBuffer is not defined. Rákeresel, találsz egy Stack Overflow választ, bemásolod a két fejlécet, működik a demó. Másnap meg kiderül, hogy a Google-bejelentkezés popupja üresen marad, mert a window.opener hirtelen null. Ez a cikk arról szól, mi történik ilyenkor a motorháztető alatt, és miért nem véletlen baleset, hanem szándékos csereüzlet.

Diagram: egy dokumentum COOP és COEP fejléccel elzárja a window.opener kapcsolatot és a jelöletlen cross-origin erőforrásokat, cserébe hozzáférést kap a SharedArrayBufferhez

Miért kellett ide egyáltalán két fejléc

2018 januárjában, a Spectre nyilvánosságra hozatala után minden nagyobb böngészőgyártó egyszerre kapcsolta ki a SharedArrayBuffer-t. A probléma nem a szinkronizált memóriával volt, hanem azzal, hogy elég pontos időméréssel a JavaScript ki tudta silabizálni, mi van a processz memóriájában – a Spectre pont ezt a fajta oldalcsatornás olvasást használja ki. Egy megosztott, pontosan mérhető memóriaterület ehhez tökéletes eszköz.

A SharedArrayBuffer viszont nem opcionális parádéfunkció: WebAssembly-szálazáshoz, multi-threaded kodekekhez és hasonló, valódi teljesítményigényes feladatokhoz szinte nélkülözhetetlen. A böngészőgyártók ezért nem törölték, hanem feltételhez kötötték a visszahozatalát. A Chrome fejlesztői blogja szerint a Chrome 88 Androidon, a Chrome 92 pedig (2021 májusában) desktopon már csak akkor engedi újra, ha az oldal úgynevezett cross-origin isolated állapotban fut. Ezt az állapotot a COOP és a COEP fejléc együtt hozza létre.

Amit a cross-origin isolation kinyit

Ha self.crossOriginIsolated értéke true, három dolog válik elérhetővé, amit egyébként a böngésző zárva tart:

  • SharedArrayBuffer létrehozása és worker közötti megosztása
  • performance.measureUserAgentSpecificMemory() – tényleges memóriahasználat lekérdezése
  • nagy felbontású, nem mesterségesen zajosított időzítők (pl. pontosabb performance.now())

Ehhez az MDN dokumentációja szerint pontosan ez a fejlécpár kell:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Egy sima Node http-szerveren ez ennyi (kipróbálva, ténylegesen ezt a két fejlécet küldi vissza):

import { createServer } from 'node:http';

createServer((req, res) => {
  res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
  res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
  res.end('<script>console.log(self.crossOriginIsolated)</script>');
}).listen(8931);

A COOP ára: elszakad a window.opener

A Cross-Origin-Opener-Policy négy értéket ismer. Az alapértelmezett unsafe-none semmit nem változtat. A same-origin viszont külön böngészési kontextus-csoportba teszi a dokumentumot minden olyan ablaktól, amivel nem azonos eredetű – ha ez a dokumentum nyitott egy cross-origin popupot (vagy őt nyitotta meg egy), a window.opener mindkét irányból null-ra vált. Pontosan ez töri el a klasszikus OAuth- és fizetési popup-mintát, ahol a bejelentkeztető ablak window.opener.postMessage(...)-al vagy közvetlen DOM-hozzáféréssel szól vissza a szülőnek.

Van egy harmadik érték, a same-origin-allow-popups, ami épp erre a helyzetre való: megtartja a popupokhoz fűződő kapcsolatot, miközben a többi cross-origin interakciót tiltja. A csapda az, hogy ez nem elég a cross-origin isolationhöz – a web.dev migrációs útmutatója is külön kiemeli, hogy a crossOriginIsolated ma kizárólag szigorú same-origin mellett lesz true. Vagyis nincs ingyenebéd: vagy megtartod a popup-alapú bejelentkeztetést, vagy megkapod a SharedArrayBuffer-t, a kettő együtt (egyelőre) nem megy natívan.

A COEP ára: minden beágyazott erőforrásnak be kell jelentkeznie

A Cross-Origin-Embedder-Policy require-corp értéke mellett minden no-cors módban betöltött cross-origin erőforrásnak (kép, iframe, script) vagy azonos eredetűnek kell lennie, vagy a Cross-Origin-Resource-Policy fejléccel kell explicit engedélyt adnia a beágyazásra. Egy térképwidget, egy fizetési iframe vagy egy régi CDN-es kép, ami erről nem tud, egyszerűen nem töltődik be – nincs hibaüzenet a UI-on, csak eltűnik a tartalom.

Erre való a harmadik COEP-érték, a credentialless: a cross-origin erőforrás CORP fejléc nélkül is betölthető, cserébe a kérés cookie-k nélkül megy ki, és a válasz sütijeit a böngésző eldobja. A MDN kompatibilitási adatai szerint ez Chrome 96 óta és Firefox 119 óta létezik, a Safari viszont még nem támogatja – tehát nem univerzális mentőöv, csak Chromium- és Firefox-alapú böngészőkben old meg gondot.

Hogyan vezesd be anélkül, hogy bármit eltörnél

A gyakorlatban nem kell az egész alkalmazást cross-origin isolate-elni. Ha csak egy konkrét oldal (egy videóvágó, egy in-browser adatbázis-eszköz) igényli a SharedArrayBuffer-t, érdemesebb azt külön útvonalra vagy aldomainre tenni, és csak arra a válaszra rátenni a két fejlécet – a bejelentkezés és a fizetési folyamat maradhat érintetlen a fő alkalmazásban.

Ha mégis az egész oldalt szeretnéd átállítani, a COEP-nek van megfigyelő üzemmódja: a Cross-Origin-Embedder-Policy-Report-Only fejléc ugyanazt a szabályt futtatja, de nem blokkol – csak riportot küld arról, mi törne el, ha élesítenéd. Ezzel előre feltérképezheted, mely beágyazott erőforrások hiányoznak a CORP fejlécből, mielőtt bárkit is elzárnál tőlük. Utána a legjobb ellenőrzés kliensoldalon egyetlen sor: if (self.crossOriginIsolated) { ... } – így a kódod ott is fut, ahol a fejlécek valamiért mégsem érnek célba (pl. egy proxy leveszi őket).

A tanulság egyszerű: a COOP és a COEP nem egy újabb biztonsági checkbox, amit bepipálsz és elfelejtesz. Konkrét, tervezhető funkcionális árat kérnek a SharedArrayBuffer-ért – és ha ezt előre tudod, jóval kevesebb idő megy el azon, hogy miért tűnt el hétfőn a bejelentkeztető ablak.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük