6 min čteníJohnny UnarJohnny Unar

StyleSmuggler není příběh o Magentu, ale o template enginu

CVE-2026-75650 umožnila neautentizované RCE na každé instalaci Magenta 2.4.4 až 2.4.9 přes email template engine. Poučení platí pro každý rendering pipeline, ne jen ten od Adobe.

co se vlastně stalo

Zhruba 72 hodin na začátku září, než Adobe vydalo nouzový patch, byla každá instalace Adobe Commerce a Magenta na verzích 2.4.4 až 2.4.9 vzdálená jediný HTTP request od neautentizované vzdálené exekuce kódu. CVSS skóre skončilo na čistých 10.0, což je číslo, po kterém lidi na pohotovosti odloží kafe. Zranitelnost dostala přezdívku StyleSmuggler, protože injection kanálem byla vlastnost styles v email template enginu, tedy pole, které platforma brala jako čistě layoutová data. Něco, u čeho čekáte, že bude držet hex barvu, font-size a nic víc. Přesně tenhle předpoklad byl celý ten bug. Hodnota styles tekla do template rendering cesty, a protože email templating v Magentu podporuje direktivovou syntaxi, která se umí rozřešit na filter metody a instanciaci bloků, útočník ovládající tenhle údajně neškodný layoutový string se dostal do code paths, které nikdy neměly být dosažitelné z neautentizovaného vstupu. Nikdo si nesedl a nerozhodl, že udělá CSS spustitelné. Stalo se to, protože rendering pipeline deset let nabaloval featury, každá jednotlivě rozumná, a někde po cestě se z datového pole stalo řídící pole. Typový systém si toho nikdy nevšiml, protože v PHP je string prostě string a nic víc.

dvoufázový řetězec

Co dělá StyleSmuggler zajímavým ke studiu, není fakt, že existuje, ale to, jak se exploit rozpadá do dvou requestů, protože jed a exekuce žijí v různých okamžicích. První fáze zasadí payload. Pošlete request, který persistuje útočníkem řízený obsah do template kontextu, v tomhle případě do vlastnosti styles připojené ke konfiguraci emailu. V tu chvíli se ještě nic nebezpečného nestalo, žádný kód neběžel, payload tam jen tak sedí a vypadá jako neškodná layoutová data, která čekají, až je něco vyrenderuje. Druhá fáze spustí render. Jakýkoli navazující flow, který skládá email s použitím té uložené hodnoty styles, teď protáhne otrávený string přes direktivový resolver. Ten resolver dělá přesně to, k čemu byl postavený, vyhodnotí direktivy uvnitř stringu a jde po nich do volání metod, které nakonec dojdou k exekuci na úrovni systému. Právě tohle oddělení je důvod, proč to spousta naivních WAF pravidel a input scannerů úplně mine. Request, který sází payload, vypadá nudně, a request, který ho odpálí, payload vůbec neobsahuje. Jen požádá aplikaci, ať pošle email. Pokud váš bezpečnostní model kontroluje jenom request nesoucí škodlivé bajty, už jste prohráli, protože v dvoufázovém řetězci nikdy nesdílí škodlivé bajty a škodlivé chování stejný síťový paket. Stejný tvar vidíme u stored XSS už dvacet let. Rozdíl je tady v tom, že sinkem je template engine se server-side exekucí místo browser DOM, a poloměr škod je celý váš stroj místo session cookie.

tohle je CWE-1336 a je to úplně všude

Server-side template injection dostala vlastní klasifikaci slabiny, CWE-1336, právě proto, že se pořád objevuje napříč naprosto nesouvisejícími stacky. Jakmile si ten tvar zvnitřníte, začnete ho vidět v code review dřív než scanner. Základní chyba je vždycky stejná: developerem řízená nebo uživatelem ovlivněná data se dostanou do template enginu jako součást samotné šablony, místo aby to byla data předaná do předkompilované šablony. Twig měl přesně tenhle problém, když aplikace staví template stringy zřetězením uživatelského vstupu a výsledek pak předá metodě render string na prostředí, která rozřeší atributy objektu a umí se dostat do _self a getFilter a odtud kamkoli. Jinja2 je tím proslulá, protože {{ config }} a procházení MRO třídy z libovolného objektu do os.popen je dneska v podstatě salonní trik, a každý Flask tutoriál, co dělá render_template_string s f-stringem, učí lidi stavět tuhle zranitelnost. Handlebars měl řetězce z prototype pollution do RCE přes rozřešení helperů. I Blade, který kompiluje do čistého PHP, se stane problémem v okamžiku, kdy někdo uloží Blade snippet do databáze a vyrenderuje ho přes Blade::render nad uživatelskými daty. Na jménu frameworku nezáleží. Záleží na strukturálním faktu: pokud string definující šablonu může ovlivnit útočník, template engine je teď interpret pro útočníkem řízený jazyk, a interprety věci spouští. Selhání Magenta nebylo nijak výjimečné, bylo úplně obyčejné, jen mělo větší install base a horší poloměr škod.

berte render vrstvu jako hranici důvěry

Myšlenková oprava spočívá v tom, přestat brát template rendering jako záležitost zobrazení a nakreslit hranici důvěry přesně u render volání, stejně jako ji už kreslíte u SQL vrstvy a u deserializace. Nikdo soudný už dneska nezřetězuje uživatelský vstup do SQL stringu, všichni reflexivně saháme po parametrizovaných dotazech, a důvod je, že jsme si zvnitřnili, že query string je kód a parametry jsou data, a tyhle dvě věci se nikdy nesmí spojit. Template enginy si zaslouží stejnou disciplínu. Šablona je kód. Kontext jsou data. Pokud tohle někdy pochází ze stejného zdroje, a obzvlášť pokud se jakákoli část toho zdroje dotýká uživatelského vstupu, máte potenciální interpreter injection a měli byste s tím zacházet stejně vážně, jako byste zacházeli s eval nad tělem requestu. Konkrétně to znamená, že množina template stringů, které vaše aplikace kdy může vyrenderovat, musí být konečná, verzovaná a známá v době deploye, nikdy skládaná za běhu z uloženého nebo odeslaného obsahu. Když klientovi auditujeme rendering vrstvu, první grep, který pustíme, hledá varianty renderování ze stringu: render_template_string ve Flasku, renderString v Twigu, Blade::render, new Function v JS templatingu, prostě cokoli, co bere tělo šablony jako argument místo cesty k šabloně. Každý zásah je nález, dokud se neprokáže opak, a „prokázat opak" znamená, že argument je compile-time konstanta s nulovou interpolací čehokoli, co kdy prošlo sítí.

jak to auditujeme, než to jde do produkce

Na vlastních projektech ve steezru i na bezpečnostních auditech, které děláme klientům na Django, Next.js a Go stacích, bereme rendering pipeline jako exekuční plochu od prvního dne. To mění pár návyků způsobem, který je dopředu levný a při zpětné úpravě drahý. Držíme tvrdou čáru mezi tím, čemu říkáme inventář šablon a data šablon: inventář je fixní množina souborů .html.twig, .jinja nebo Blade, které existují v repu, a všechno, co kdy dodá uživatel nebo admin, jde do kontextového slovníku, nikdy do těla šablony. Bez výjimek, ani pro marketing, který strašně chce nechat merchandisery editovat email layouty. Když nějaká featura opravdu potřebuje uživatelsky editovatelné šablony, a e-commerce email buildery jsou klasický případ, který sem Magento dostal, ten obsah nepředáme reálnému enginu. Prožene se to schválně slabým, sandboxovaným rendererem s allowlistem direktiv a nulovým přístupem k vnitřkům objektů, rozřešení filterů nebo reflexi, protože template jazyk, který umí jen dosazovat pojmenované proměnné a iterovat přes seznamy, nemůže nic propašovat. Taky počítáme s dvoufázovým útokem, takže uložená data, která nakonec dorazí do rendereru, se validují už při zápisu proti gramatice, kterou mají skutečně mít. To znamená, že pole styles se naparsuje jako CSS a odmítne se, pokud obsahuje direktivovou syntaxi, místo abychom věřili, že bude neškodné až při čtení. Až spadne příští StyleSmuggler, patchněte rychle, samozřejmě, a pokud provozujete Magento, měli byste už dávno běžet na opravené verzi. Trvalá výhra je ale navrhnout to tak, aby pole, po kterém útočník sáhne, nikdy nebylo napojené na interpret. Z architektury, která bere layoutové stringy jako spustitelný kód, se totiž patchováním nedostanete.

Johnny Unar

Napsal/a

Johnny Unar

Chcete s námi spolupracovat?

CVE-2026-75650 umožnila neautentizované RCE na každé instalaci Magenta 2.4.4 až 2.4.9 přes email template engine. Poučení platí pro každý rendering pipeline, ne jen ten od Adobe.