Parser může být bezchybný a přesto nebezpečný, když ostatní čtou jinak
Představ si, že jeden nástroj archiv zkontroluje a druhý ho rozbalí. Oba fungují. Jenže kontrola viděla jiné soubory než instalace. Tomu se říká parser differential a je to samostatná třída chyb, protože nežije v žádném jednom parseru, ale v rozdílu mezi dvěma. Astral kvůli ní pro uv napsal nový parser tar-codec. Jeho rozbor stojí za čtení i bez zájmu o tar, protože ukazuje, co dělá formát na tuhle třídu chyb náchylným.
Tar je tři formáty a volné místo
Archiv se skládá z bloků po 512 bajtech, včetně hlaviček. Soubor o 513 bajtech zabere nejméně 1 536 bajtů. Hlavička, dva bloky dat a v tom druhém výplň. Výplň má být podle konvence nulová, ale nic to nevynucuje, takže se do ní dá schovat cokoli.
Pak přichází historie. Rodiny UStar/pax, GNU a V7 mají každá vlastní pravidla pro přepis jména, cíle odkazu i velikosti. Pax záznam říká, že další člen má 8000 bajtů, i když jeho hlavička uvádí něco jiného. To je legitimní a nebezpečné zároveň. Velikost určuje, kde začíná další hlavička, takže parser, který pax záznam přehlédne, se od té chvíle dívá na jiné bajty. Některé implementace například přijmou hlavičku g se size=1234, ale velikost ignorují. Autoři to popisují jako nástroj pro úplnou desynchronizaci. Jeden parser vidí neškodné soubory, druhý útočníkovy.
Je to stejný vzorec jako HTTP request smuggling, kde se proxy a backend neshodnou na konci requestu. Kdekoli délka určuje hranici dalšího záznamu a existují dva zdroje délky, tam se dá vyrobit neshoda.
Shovívavost se dodatečně neopraví
Astral dříve používal parser astral-tokio-tar a nakonec došel k závěru, že rodina tokio-tar je „příliš shovívavá už z návrhu“. Dodatečné přidávání omezení by bylo pracnější a náchylnější k chybám než nový parser. Důvod je strukturální. Shovívavý parser sloučí stavy, které měly zůstat oddělené, a pak už nemá podle čeho odmítat.
tar-codec proto staví na tom, že neplatný stav nejde vyjádřit. Rodinu určí z prvního bloku hlavičky a archiv míchající UStar/pax s GNU odmítne. U GNU smí na člena působit nejvýš jedno rozšíření L a jedno K. Pax záznam má tři stavy: nedeklarovaný, aktivní a smazaný. Záznamy se čtou podle délkového prefixu, ne dělením na nových řádcích. Čísla se parsují přísně, aby dvě implementace nepřečetly stejnou size různě.
Přísnost má ale hranici danou realitou. GNU tar i star historicky zapisovaly binární záznamy, i když pax vyžaduje UTF-8. tarfile z CPythonu umí zaplnit uname do 32 bajtů, i když specifikace dovoluje 31. A mode nebo mtime bývají celé nulové, takže je tar-codec eviduje jako chybějící, místo aby si domýšlel výchozí hodnotu. Každá taková výjimka je vědomé rozhodnutí, ne vedlejší efekt. Přesto autoři píšou, že absenci budoucích neshod zaručit nemůžou.
Co si z toho odnést
Až bude v systému kontrola a použití téhož vstupu ve dvou krocích, ptej se, jestli oba kroky čtou totéž. Nejbezpečnější je rozparsovat vstup jednou a dál předávat výsledek, ne původní bajty. U vlastních formátů nedovol dva zdroje pravdy pro délku a nejednoznačný vstup odmítni, místo aby ses ho snažil pochopit. Postelovo pravidlo o shovívavém přijímání je tady přímo rizikem. Shovívavost je rozhodnutí, které se špatně bere zpět, protože na ní začnou záviset cizí soubory.