Opravu kritické díry v SGLangu zavřel správce spolu s dalšími návrhy téhož účtu
SGLang vyhodnocuje ladicí filtr funkcí eval() a přes ni jde v procesu scheduleru spustit cizí kód. Nahlásil to 14. října Jiapeng Li, o 48 minut později poslal někdo jiný opravu a téhož dne večer ji správce zavřel v jednom balíku s ostatními návrhy toho účtu, který zároveň zablokoval. Záznam CVE vyšel 16. října, volání eval() je v hlavní větvi dál.

V ladicím nástroji serveru SGLang je jeden řádek, který zadaný text nevyhodnotí jako podmínku, ale jako program. Kdo se k němu dostane, spustí vlastní kód v procesu, který na serveru rozvrhuje práci modelu. Nahlásil to 14. října hlášení #44188 a 16. října k tomu vyšel záznam CVE-2026-111391. Volání, o které jde, je v hlavní větvi dál.
Prázdné builtins nejsou klec
SGLang umí při ladění vypisovat vnitřní tenzory a omezit výpis podmínkou, kterou obsluha zadá jako text. Tu podmínku vyhodnocuje funkce _evaluate_filter():
_FILTER_BUILTINS: dict[str, Any] = {"search": re.search, "match": re.match}
def _evaluate_filter(filter_expr: str, tags: dict[str, Any]) -> bool:
namespace = _DefaultNoneDict(tags)
namespace.update(_FILTER_BUILTINS)
return bool(eval(filter_expr, {"__builtins__": {}}, namespace))
Zápis {"__builtins__": {}} má být ohrádka: vyprázdní seznam vestavěných funkcí, takže zadaný výraz nemá čím otevřít soubor ani načíst modul. Jenže o řádek výš se do jmenného prostoru vloží dvě funkce z modulu re, aby se ve filtru dalo hledat podle vzoru. A funkce v Pythonu si nese odkaz na modul, ve kterém vznikla, včetně jeho vestavěných funkcí. Prázdná ohrádka tedy platí jen pro výraz, který si přes ty dvě funkce nesáhne zpátky.
Autor hlášení to píše bez obalu: prázdné builtins nejsou bezpečnostní ohrádka, protože volatelné objekty si drží odkazy na své vlastní globální jmenné prostory. Citace je přeložená.
Filtr se nevyhodnocuje při zadání, ale až při výpočtu modelu. Zadaný kód se proto spustí ve scheduleru při nejbližším průchodu dopředu, tedy u prvního dalšího požadavku, který se k ladicímu bodu dostane. Server přitom odpoví normálně. Je to týž vzorec jako u Xinference, kde se jako kód spouštěla odpověď modelu; rozdíl je v tom, odkud ten text přijde.
Kdo se k tomu řádku dostane
Cesta /dumper/{method} se na rozhraní modelu zaregistruje jen tehdy, když se proměnná prostředí DUMPER_SERVER_PORT rovná přesně řetězci reuse. Popis v registru mluví obecněji – „když je DUMPER_SERVER_PORT nastavená“ –, což je širší než ta podmínka v kódu. Běžně spuštěný server tedy tu cestu nemá vůbec.
Když ji má, rozhoduje o přístupu úroveň ADMIN_OPTIONAL. Ze souboru auth.py plynou čtyři případy:
- bez jakéhokoli klíče je cesta otevřená komukoli;
- s nastaveným
api_keystačí ten obyčejný uživatelský klíč; - s nastaveným
admin_api_keyje potřeba klíč správce; - když jsou nastavené oba, projde jedině klíč správce a obyčejný
api_keyse odmítne.
Záznam CVE označuje koncový bod za nechráněný. To platí pro první případ. Pro druhý, který v hlášení předvádí autor, platí něco jiného a horšího pro provozovatele: kdo komukoli vydá obyčejný klíč k modelu, vydal mu zároveň ladicí rozhraní. Poslední odrážka je přitom obrana, kterou SGLang má – nastavený admin_api_key ten koncový bod zamkne.
Oprava přišla za 48 minut a zůstala zavřená
Osmačtyřicet minut po hlášení poslal někdo třetí návrh #44205: tři soubory, 310 přidaných a 94 ubraných řádků, které eval() vymění za vyhodnocovač postavený nad syntaktickým stromem s povolovacím seznamem uzlů a zákazem přístupu k atributům se dvěma podtržítky. Kontrola stylu v CI nejprve spadla, autor ji odpoledne srovnal.
Ve 20.40 téhož dne návrh zavřel správce projektu, nesloučený, a důvod napsal rovnou do něj: zavírá ho spolu s ostatními otevřenými návrhy toho účtu a účet blokuje. Za dva dny z něj podle správce přišlo 37 návrhů, žádný sloučený – skoro shodné obměny téže změny, znovu poslané věci zavřené den předtím, zkušební návrhy a zásahy do chování bez testu, postupu k zopakování i čísel. „Každý návrh tady stojí správce čas na revizi a běh CI,“ napsal (přeloženo).
Ten důvod se opravy netýká, týká se účtu. Jiný návrh k tomu hlášení ale nikdo nepodal – je to jediný – a hlášení zůstává otevřené. V hlavní větvi stojí volání eval() s prázdnými builtins dál, na řádku 1320.
Záznam mají tři registry a hlídač nehlásí nic
Číslo přidělil VulnCheck a zařadil chybu pod CWE-95, tedy vyhodnocení cizích direktiv. Známku dal dvakrát a vyšla jinak: podle CVSS 3.1 je to 8,1 a vysoká závažnost, podle CVSS 4.0 je to 9,2 a kritická. Rozdíl dělá hlavně obtížnost útoku, kterou starší metodika hodnotí jako vysokou a novější jako nízkou s podmínkou souběhu. Slovo „kritická“ tedy pochází z novější stupnice téhož zadavatele.
Co se z registrů naopak nedozví žádný hlídač závislostí: databáze NVD vede záznam ve stavu Deferred, což znamená, že ho nikdo z NVD analyzovat nebude, a nemá u něj ani jeden strojově čitelný údaj o dotčeném produktu. Databáze GitHubu ho vede jako nekontrolovaný a seznam dotčených balíčků má u něj prázdný. Dependabot proto na SGLang kvůli téhle chybě nikoho neupozorní, i když záznam v jeho vlastní databázi je.
Které verze to mají
Zadavatel uvádí rozsah od 0.5.10 do 0.5.22 včetně. Sedí: ve značce v0.5.9 soubor dumper.py ani seznam _FILTER_BUILTINS, ani vyhodnocení s prázdnými builtins neobsahuje, kdežto ve v0.5.10, v0.5.11, v0.5.21 i v0.5.22 obsahuje obojí. Značka v0.5.10 nese datum 5. dubna 2026, takže ten řádek v kódu stojí přes půl roku.
Horní hranice je ale značka, pod kterou žádné vydání nevyšlo. Nejnovější zveřejněné vydání je v0.5.21 z 2. října; značku v0.5.22 někdo založil 15. října ve 23.39 na vydávací větvi se zprávou „Release v0.5.22“, vydání k ní ale v repozitáři není. Kdo tedy běží na poslední vydané verzi, má ten řádek v kódu stejně jako ti před ním.
Zdroje: hlášení #44188 a návrh opravy #44205 v repozitáři SGLangu, zdrojové soubory dumper.py, http_server.py a auth.py ve značce v0.5.22, hlášení VulnChecku a záznam GHSA-vfgj-p7f7-r3v8.