Izolaci neprorazil útok na sandbox, ale kód mimo něj, který rámci věřil
HTML Preview v ChatGPT dělal přesně to, co měl. Běžel v omezeném rámci na odděleném originu a na stránku chatgpt.com nedosáhl. Výzkumníci z Parameter ho přesto dokázali proměnit v libovolný JavaScript běžící jako chatgpt.com, u kohokoli přihlášeného, kdo otevřel sdílený odkaz. Origin chrání čtení, ale rámec může pořád posílat zprávy. Bezpečnost tak závisí na každém kódu v hlavní stránce, který zprávy přijímá.
Řetěz složený z rozumných rozhodnutí
Začíná to běžným interaktivním grafem. Ten načetl knihovnu MathLive a ta si na hlavní stránce zaregistrovala posluchače zpráv mathlive#virtual-keyboard-message pro virtuální klávesnici. Origin policy měl nastavenou na none, takže bral zprávy odkudkoli, a u zprávy update-setting neověřoval ani odesílající okno. Rámec z Preview tak mohl vyměnit rozložení klávesnice.
Rozložení přijímá HTML a knihovna ho zapsala přes innerHTML. Pomocná funkce createHTML vypadá jako sanitizace, ale byla to jen obálka, která vstup vrátí beze změny. Její jméno naznačuje ochranu, kterou neposkytuje.
innerHTML skripty nespouští, takže útok potřeboval další krok: iframe se srcdoc a bez atributu sandbox. Takový rámec vytvořený hlavní stránkou zdědí její origin. Zbývala Content Security Policy, která inline skripty blokovala, ale povolovala YouTube. Endpoint oEmbed na YouTube vrátil parametr callback do odpovědi ještě před chybou validace. Stačilo ho zakončit ;void, aby připojený objekt s chybou nevadil, a prohlížeč odpověď spustil jako skript z povolené domény.
Odtud je to jednoduché. Kód přečetl z dat stránky přístupový token a volal /backend-api/conversations, včetně čtení cizí soukromé konverzace.
Proč to nikdo neviděl jako celek
Žádný článek sám o sobě nevypadá jako díra. Graf je neškodný a Preview je izolovaný. MathLive je knihovna pro zápis matematiky. Samostatná příloha zprávy poslat uměla, ale nikdo je neposlouchal. Posluchač vznikl jen díky grafu, takže exploit potřeboval graf i Preview v jedné veřejné konverzaci. Testy jednotlivých funkcí tenhle stav nikdy nenavodí.
Podstatné je, kde se o důvěře rozhodlo. Posluchače nenapsal tým, který navrhoval sandbox. Přišel se závislostí, kvůli funkci, která se sandboxem nesouvisí. Návrh izolace přitom počítá s tím, že hlavní stránka je důvěryhodné území. Každá knihovna s posluchačem message tenhle předpoklad tiše mění, protože otevírá kanál, kterým může mluvit každý rámec na stránce.
Podobně dopadla CSP. Povolit doménu znamená povolit každý její endpoint, který umí vrátit spustitelnou odpověď. JSONP z tvé politiky takovou doménu udělá vektorem, i když její provozovatel nic špatného nedělá. Proto ji autoři řadí mezi obranu do hloubky, ne mezi hranici, na které můžeš stavět.
Co si z toho odnést
Až budeš renderovat cizí obsah v rámci, nepovažuj práci za hotovou po nastavení originu. Vypiš si, kdo na hlavní stránce přijímá message, včetně závislostí, a u každého posluchače vyžaduj kontrolu event.origin i event.source. Dynamicky vytvářené rámce nech v sandbox a do innerHTML nepouštěj nic, co přišlo zvenčí, ať se pomocná funkce jmenuje jakkoli. Sdílený odkaz ber jako vstup od útočníka. A testuj kombinace funkcí, protože tady vznikla díra až jejich složením.