Learning
Všechny články
Engineering28. září 2026 5 min

Escapovat musíš podle kontextu, ne podle formátu

Převodník logů psal do atributu, ale escapoval, jako by psal text

Zranitelnost nebyla v tom, co se nefiltrovalo, ale v tom, do jaké gramatiky se to vkládalo

Terminály umí hyperlinky. Sekvence OSC 8 má tvar ESC ]8;;URL BEL text ESC ]8;; BEL a znamená: tenhle text je odkaz na tuhle adresu. Knihovna ansi2html z toho při zobrazení build logu na webu vyrobí <a href="URL">text</a>.

Přesně tady se to zlomilo. Vstup byl pořád text. Výstup už ale nebyl text, byl to HTML atribut, a ten má vlastní pravidla pro to, co ho ukončuje. URL se do něj vložila tak, jak přišla.


Uvozovka v URL vypnula atribut

Útočník do adresy vložil uvozovku a za ni další atributy:

ESC ]8;;https://example.com/"/autofocus/tabindex="1"/onfocus="alert`xss`" BEL text BEL

Prohlížeč to přečetl jako:

<a href="https://example.com/" autofocus tabindex="1" onfocus="alert`xss`">text</a>

Uvozovka za example.com/ atribut href uzavřela a všechno za ní se stalo dalšími atributy značky. autofocus zajistil, že element focus dostane sám, a onfocus je spustitelný handler. Knihovna navíc přijímala javascript: jako protokol přímo.

Dopad byl na SourceHut plný. Kdo si zobrazil nakažený log na builds.sr.ht se zapnutým JavaScriptem, propůjčil útočníkovi svou session. Skript si ze stránky vytáhl CSRF token a mohl odeslat vlastní build job, dojít si k právům administrátora a k deploy klíčům infrastruktury samotného SourceHutu. Protože ten build job umí zapsat další log, šlo to zřetězit do červa.


Jeden výstup, tři gramatiky

Zajímavá je struktura chyby, ne konkrétní payload. Převodník stál na hranici mezi formáty a na každé straně té hranice platí jiná pravidla o tom, co je nebezpečný znak.

V terminálové sekvenci je hranicí BEL. V HTML textu jsou nebezpečné <, > a &. V HTML atributu je nebezpečná uvozovka. A v atributu, který začíná na on, je obsah kód v JavaScriptu, takže tam nepomůže žádné escapování, jen to, že takový atribut nikdy nesmí vzniknout ze vstupu. Čtyři různé odpovědi na otázku, co escapovat.

Kód, který tyhle vrstvy nemodeluje, nemá jak mít správnou odpověď, protože v okamžiku psaní nezná kontext. Autor to říká napřímo: knihovně chyběla struktura, potřebovala by přepsat na stavový automat. To není estetická poznámka. Stavový automat drží informaci, do jakého kontextu právě zapisuje, a escapování z ní pak vyplývá místo aby se doplňovalo ad hoc.

Opravy sedí na obou úrovních. SourceHut přidal serverovou sanitizaci výstupu z ansi2html, tedy záplatu na hranici, kterou vlastnil. Upstream ve verzi 1.9.4 začal URL validovat a escapovat před vložením do HTML, tedy opravil to místo, kde informace o kontextu skutečně byla.


Co si z toho odnést

Když píšeš cokoli, co jeden formát převádí na druhý, je otázka správnosti vždycky ve tvaru "do jaké gramatiky právě zapisuji", nikdy ve tvaru "jaké znaky jsou špatné". Seznam špatných znaků je důsledek, ne příčina, a při každém přechodu kontextu se mění.

Praktické pravidlo pro generování značkování: nikdy neslepuj řetězce. Skládej strom a nech serializaci, aby escapovala podle pozice. Když to není možné, měj v kódu explicitní stav a k něj vázanou escapovací funkci — escapeAttr a escapeText nejsou synonyma.

A zvlášť u atributů: povol jen ty, které chceš. Rozdíl mezi bezpečným a zranitelným kódem tady nebyl v tom, jak se escapovala hodnota href, ale v tom, že mohl vzniknout atribut, o kterém autor knihovny nikdy nerozhodl.

Zdroje

Poslech místo čtení — 1 zdroj k načtení.