У 2025 році в vLLM — одному з найпопулярніших open-source inference-рушіїв для LLM — виявили CVE-2025-9141: вразливість, через яку XML-парсер tool-call аргументів для моделі Qwen3 Coder передавав майже будь-який вхідний рядок напряму в Python-функцію eval(). Це означало, що сама модель, генеруючи звичайну відповідь, могла виконати довільний код на машині, де крутиться inference-рушій, — тій самій машині, що тримає її ваги в GPU-памʼяті.

Баг закрили. Але суть проблеми лишилась відкритою: індустрія роками виходила з припущення, що вихідні токени LLM — це дані. Текст, який показують користувачу, аргументи, які передають у функцію, рядки, які логують. Дані не виконують — їх лише читають. CVE-2025-9141 показала протилежне: якщо софт, що приймає ці токени, недостатньо обережний, дані перетворюються на код. І це не одинична випадковість у коді одного мейнтейнера — це системна властивість того, як побудовані сучасні inference-стеки.

Чому це критично саме зараз? Бо хост, на якому крутиться LLM, — не рядовий сервер. Це машина з достатньою обчислювальною потужністю для фронтир-моделі, прямим доступом до її ваг і, як правило, привілейованим місцем у мережі дата-центру порівняно зі звичайним інтернет-хостом. Захопити такий вузол — це не «поцупити паролі з ноутбука розробника», а отримати плацдарм усередині інфраструктури, що обслуговує AI-продукт.

Дослідник Бойд Кейн розібрав цей клас атак у розлогому есе, розклавши по кроках, як токен-послідовність може стати експлойтом, чому мультимодальність підвищує ставки і що з цим можна вдіяти вже сьогодні. За даними HackerNews, де есе набрало помітну дискусію, — це саме та тема, яку розробники self-hosted LLM-інфраструктури мали б прочитати перед наступним деплоєм. У AiiN розкладаємо її під практичним кутом: не «а раптом», а що саме ламається і що з цим робити.

Звідки взялася ця вразливість токен-парсерів?

Сучасні агентні контури — Claude Code, Codex, будь-який кастомний ReAct-агент — типово рознесені на дві машини. Одна виконує дії: читає файли, стукає в API, комітить код. Друга, inference-хост, рахує токени на GPU і віддає відповідь назад. Ця архітектурна межа десятиліттями сприймалась як межа довіри: те, що приходить із GPU-хоста, — просто текст, а його подальшу долю вирішує вже агент.

Але сам inference-хост — теж повноцінний застосунок, і застосунок дуже непростий. Документація vLLM перелічує підтримку понад 200 архітектур моделей, а лише в директорії прикладів проєкту лежить близько 35 Jinja-шаблонів чату під різні формати розмітки tool-call, ролей і reasoning-блоків. Кожен такий шаблон — окремий парсер, який мусить перетворити сирий потік токенів у структуровану розмову: хто сказав що, де почався виклик інструмента, де закінчився reasoning. Що більше форматів підтримує рушій, то більше шансів, що якийсь із парсерів написаний недбало.

Саме так зʼявилась CVE-2025-9141. XML-based tool-парсер для Qwen3 Coder мав передавати рушію лише структуровані аргументи виклику функції. Замість цього він прогонив майже будь-який отриманий рядок через eval() — інтерпретатор Python виконував рядок як код, а не як дані. Автоматичний рев'ю-бот Gemini проаналізував PR, що вносив цей код, і коректно позначив його як критичну вразливість. Попри це попередження, провідний мейнтейнер vLLM форснув мердж зі словами «force merging this to unblock model usage» — щоб не блокувати підтримку моделі. Патч на eval() вийшов пізніше, вже після публічного розголосу.

Як саме токени перетворюються на код, що виконується на хості?

Технічно шлях токена до відповіді користувачу — конвеєр із кількох кроків: GPU рахує логіти, семплер обирає наступний токен, детокенізатор перетворює ID токена назад у текст, а далі — найцікавіша частина — парсер розбирає накопичений текст на структуру: що з цього звичайна відповідь, що виклик інструмента з аргументами, що внутрішній reasoning-блок, який не варто показувати користувачу. Саме на цьому останньому кроці вразливості й зʼявляються, бо парсер повинен «вгадати» намір моделі за текстовим форматом, а не за жорсткою бінарною структурою.

У випадку CVE-2025-9141 парсер спростив собі задачу і замість власного розбору XML-атрибутів виклику інструмента просто віддав рядок в eval() — швидко для розробника й фатально небезпечно, бо eval() виконує будь-який Python-вираз, що туди потрапить, включно з імпортом модулів на кшталт os чи subprocess.

Але й без такого відвертого eval()-кошмару парсери ламаються на рівному місці. В одному з issue vLLM користувач тестував, як модель MiniMax-M3 веде reasoning, і модель у відповіді просто процитувала рядок <mm:think> — не як службовий тег, а як звичайний текст у відповідь на прохання «повтори точно те, що я написав». Парсер vLLM сприйняв цей рядок буквально й почав відрізати частину відповіді в окремий reasoning-блок, хоча жодного reasoning там не малося на увазі. Наслідок тут нешкідливий — сплутана відповідь, а не виконання коду, — але механізм той самий: парсер довіряє формату тексту більше, ніж мав би, і будь-який збіг символів здатен звести з пантелику всю подальшу обробку.

А що з голосом і зображеннями?

Мультимодальні моделі, які відповідають зображенням чи аудіо, теоретично відкривають ширший канал для атаки — вища пропускна здатність, більше форматів файлів, а декодери зображень і аудіо історично були улюбленою мішенню malformed-file експлойтів проти ffmpeg чи інших медіа-бібліотек. На практиці ризик поки нижчий, ніж могло б здатися: більшість мультимодальних LLM генерують не сирі байти файлу, а обмежений набір «медіа-токенів», які окремий, специфічний для конкретної моделі декодер перетворює на пікселі чи звукову хвилю вже після того, як мовна частина моделі закінчила роботу. Це відсікає найпростіший клас атак через биті файли. Але сам декодер, енкодер і нативні CUDA-кернели навколо них — додатковий, ще менш перевірений код, і що активніше лабораторії оптимізовують ці шляхи заради швидкості, то вища ймовірність, що там теж знайдуться діри.

Чим це відрізняється від класичного prompt injection?

Класичний prompt injection — коли зловмисний текст (вебсторінка, документ, лист) підказує моделі зробити щось, на що в неї, по суті, є права: надіслати лист, видалити файл, викликати чутливий інструмент. Атака, яку описує Кейн, — іншої природи: вона взагалі не потребує, щоб модель «захотіла» зробити щось шкідливе в сенсі інструментів. Досить, щоб послідовність токенів — байдуже, яка вона за змістом, — збіглася з патерном, що ламає парсер. Атака відбувається не на рівні смислу («зроби X»), а на рівні синтаксису самого рушія («ці символи виконай як код»). Це різниця між соціальною інженерією і переповненням буфера: обидва небезпечні, але лагодяться геть по-різному.

Порівняння з іншими рушіями тут доречне. vLLM і SGLang — найпопулярніші opensource inference-стеки для self-hosted LLM, обидва оптимізовані передусім під пропускну здатність і latency, а не під ізоляцію процесів. TensorRT-LLM від NVIDIA та llama.cpp мають меншу поверхню атаки за рахунок вужчого набору підтримуваних форматів, але тим самим менш гнучкі для продакшн tool-calling сценаріїв, де й трапляються такі баги. Закриті inference-стеки OpenAI, Google DeepMind чи Anthropic для власних фронтир-моделей теж напевно не бездоганні, але в них є те, чого бракує швидкоплинній open-source-екосистемі: виділені security-команди, що проганяють red-team проти внутрішньої інфраструктури до релізу, а не після скарги в issue-трекері.

Ринок self-hosted inference — це ще й хмарні GPU-провайдери на кшталт RunPod, Together AI, Fireworks чи Modal, які надають multi-tenant хостинг для чужих моделей. Там одна вразливість парсера — вже не проблема одного деплою, а потенційний вихід за межі ізоляції між клієнтами на спільному залізі.

Що це дає тим, хто будує на цьому зараз?

Для команд, які вже запустили чи планують self-hosted LLM у себе — на vLLM, SGLang чи будь-якому їхньому форку, — це не абстрактна погроза з блогу дослідника безпеки, а конкретний пункт у чеклисті перед продакшном. Кілька сценаріїв, де це відчутно вже сьогодні:

Практичні кроки, які має сенс перевірити вже зараз:

Що може піти не так із захистом, який тут пропонують?

Автор есе пропонує архітектурний захист: рознести GPU-хост і токен-парсер на різні машини. GPU-бокс віддає лише логіти — сирі числа ймовірностей для наступного токена, — а другий хост уже семплить токен, розбирає його на структуру чату й передає далі в агентний харнес. Ідея логічна: навіть якщо парсер на другому хості зламають, зловмисник отримає контроль над CPU-машиною без прямого доступу до GPU-памʼяті, де лежать ваги моделі.

Але в цієї ідеї є ціна. Сучасні inference-рушії тримають семплінг максимально близько до GPU навмисно, заради latency: передавати логіти по мережі для кожного токена, особливо при батчингу й спекулятивному декодуванні кількох кандидатів одночасно, означає додаткові мілісекунди на кожному кроці генерації, помножені на мільярди токенів на добу для будь-якого продакшн-навантаження. Для команд, що змагаються за token/sec на GPU-годину, це реальний компроміс, і немає гарантії, що галузь масово на нього піде, поки не станеться публічний інцидент, дорожчий за втрачені мілісекунди.

Друга проблема глибша за архітектуру: сам факт, що провідний мейнтейнер vLLM свідомо форснув мердж коду з eval() попри автоматичне попередження, — це не технічний, а культурний збій. Тиск «розблокувати підтримку моделі якнайшвидше» в опенсорсному проєкті, що щомісяця обростає новими архітектурами й шаблонами чату, повторюватиметься знову й знову, доки швидкість фіч важливіша за процес рев'ю безпеки. Це структурна, а не одноразова проблема.

Є й відкриті питання без відповіді. Наскільки реально для моделі самостійно знайти й відтворити такий експлойт без підказки? Сам автор есе визнає невпевненість — вважає це «доволі ймовірним», але наголошує, що знайти вразливість складніше, ніж її застосувати, і фронтир-модель, яка досліджує кодову базу vLLM у процесі роботи, цілком може натрапити на дірку випадково. Ще один вектор, менш очевидний: якщо LLM дедалі частіше пишуть чи оптимізують C++/CUDA-код інференс-рушіїв заради швидкодії, зловмисна чи скомпрометована модель отримує шанс закласти out-of-bounds memory access просто в низькорівневий код рушія — а такий бекдор набагато важче виявити рев'ю, ніж рядок з eval().

Часті запитання

Чи означає це, що self-hosted LLM небезпечніші за хмарні API? Не автоматично — хмарні API теж працюють на inference-рушіях, просто з менш публічною інформацією про внутрішню архітектуру та, ймовірно, з виділеними security-командами. Ризик вищий там, де рушій свіжий, мало перевірений і доступний з мережі без ізоляції, — а це типовий профіль швидкого self-hosted деплою на vLLM чи SGLang, а не наслідок самого факту self-hosting.

Чи закрили CVE-2025-9141 остаточно? Патч на конкретний виклик eval() вийшов, і вразливість позначена як усунена в GHSA-адвайзорі проєкту. Але клас проблеми — довіра до сирого тексту від моделі на рівні парсера — лишається відкритим і, найімовірніше, ще проявиться в інших парсерах чи інших рушіях.

Висновок AiiN

Головний висновок AiiN із цієї історії простий і неприємний: індустрія inference-рушіїв зараз проходить той самий шлях, який браузери пройшли на початку 2000-х, коли довільний JavaScript із будь-якого сайту виконувався в тому самому процесі, що й решта браузера, доки роками боли й пісочниці рендерера не зробили site isolation стандартом. LLM-рушії сьогодні — на до-пісочницевій стадії: токени моделі досі здебільшого трактують як напівдовірені дані, а не як вхід від потенційно ворожого джерела, хоча по суті це саме він і є, щойно модель бере участь у RAG чи агентному контурі із зовнішнім контентом.

Прогноз AiiN на найближчі 6–12 місяців: очікуємо ще щонайменше кілька нових CVE в vLLM, SGLang та їхніх найближчих аналогах — просто тому, що список підтримуваних архітектур і форматів чату росте швидше, ніж встигає рости security-рев'ю навколо нього. Ізоляція GPU-хоста від хоста, що парсить токени, найближчим часом залишиться нішевою практикою «для тих, хто вже читав такі есе», а не дефолтом коробкових деплоїв — поки не станеться перший публічний інцидент, що вдарить по грошах чи репутації якогось помітного self-hosted гравця. Цілком імовірно, що саме тоді зʼявиться перший комерційний прошарок продуктів на кшталт «secure inference gateway» — по суті WAF, але для токенів LLM, а не HTTP-запитів.

Наша практична порада тим, хто зараз піднімає self-hosted LLM: ставтеся до inference-хоста так, ніби він уже частково скомпрометований, з першого дня деплою. Мінімальні мережеві права, окремий сегмент без прямого доступу до секретів і вихідний трафік лише туди, куди справді треба, — це дешевше зробити зараз, ніж після CVE. За даними HackerNews, дискусія навколо цього есе вже зрушила частину спільноти в бік такого мислення — питання лише, наскільки швидко це стане стандартом, а не винятком.