Перейти к основному содержанию
Блог

Веб

SSTI: инъекция на стороне сервера через шаблоны (Twig/Blade)

Если текст пользователя передаётся прямо в шаблонизатор, он может быть исполнен как код, а не выведен как текст. Как возникает SSTI, как её проверить и как закрыть в Twig/Blade.

· 6 мин. чтения

Фото: Dinkun Chen, CC BY-SA 4.0 · Wikimedia Commons

Про SQL-инъекцию слышал почти каждый разработчик, но инъекция через шаблоны (SSTI — Server-Side Template Injection) известна меньше, хотя последствия могут быть не менее серьёзными: выполнение произвольного кода на сервере (RCE).

Откуда берётся проблема

Шаблонизаторы вроде Twig, Blade, Jinja2 или FreeMarker делают две вещи: выводят статический текст и вычисляют выражения внутри {{ }}. Если приложение обрабатывает введённый пользователем текст как сам шаблон, атакующий присылает вместо обычного текста синтаксис шаблона, и движок выполняет его как код.

Простой пример — построение текста сообщения так:

$template = "Привет, " . $request->name . "!";
return Blade::render($template);

Здесь текст пользователя повторно рендерится как сам шаблон. Если вместо name прислать {{ phpinfo() }} или {{ system('id') }}, Blade воспримет это как выражение и выполнит. Та же ошибка встречается и в Twig: передача текста пользователя через $twig->createTemplate($userInput)->render() — классическая дверь для SSTI. Для сравнения: обычный вызов view('welcome', ['name' => $request->name]) безопасен, потому что name здесь только значение, а не шаблон.

Почему это приводит к RCE

Обычное выражение в шаблоне может быть математической операцией или обращением к переменной, но большинство шаблонизаторов позволяют и вызывать методы объектов. В Twig и Blade под капотом работает сам PHP, поэтому правильным payload можно добраться до файловой системы, переменных окружения или даже функций выполнения команд. В итоге SSTI часто превращается в полный контроль над сервером — одно из самых тяжёлых проявлений категории инъекций.

Шаблонизатор — это небольшой язык программирования. Не передавайте в него текст пользователя как «шаблон», только как «данные».

Как проверить

Самый простой способ — отправить математическое выражение и посмотреть на результат:

name=${7*7}
name={{7*7}}
name=#{7*7}
name=<%= 7*7 %>

Если в ответе вместо текста появилось 49 — значит, введённое значение было выполнено как шаблон. То, какой синтаксис сработал, помогает определить движок: {{ }} — стиль Twig/Jinja2/Blade, ${ } — FreeMarker/Velocity, <%= %> — ERB.

Составьте список всех мест, где текст пользователя может использоваться как «шаблон» — поля форм, шаблоны писем, генераторы PDF, тексты уведомлений — и отправьте эту же пробу в каждое:

curl -s https://your-domain.uz/contact \
  --data-urlencode "message={{7*7}}" | grep -o "49"

Как исправить

1. Никогда не рендерите текст пользователя как шаблон

Самое надёжное исправление — вообще не использовать введённый пользователем текст как источник шаблона. Шаблоны должны быть файлами, написанными разработчиком и хранящимися в системе контроля версий; данные пользователя передаются только как значения переменных:

// Опасно: текст пользователя — источник шаблона
return Blade::render($request->input('template'), $data);

// Правильно: текст пользователя — только значение
return view('messages.welcome', ['name' => $request->input('name')]);

2. Если нужен динамический шаблон — песочница

Иногда бизнес-требования заставляют дать пользователю возможность писать свой шаблон (например, конструктор писем). В этом случае используйте SandboxExtension Twig — он блокирует вызов неразрешённых функций и методов. У Blade готовой песочницы такого рода нет, поэтому избегайте рендеринга пользовательского шаблона через Blade; вместо этого используйте ограниченную замену переменных (например, вручную подставляйте простые placeholder'ы вида {{ имя }}).

3. Экранирование вывода остаётся обязательным

SSTI и XSS — смежные проблемы: даже после закрытия инъекции шаблона вывод должен корректно экранироваться. В Blade {{ }} экранирует автоматически, а {!! !!} — нет; не выводите текст пользователя через {!! !!}.

Почему эта ошибка повторяется

SSTI часто появляется под видом «удобства»: разработчик хочет разрешить редактировать шаблон письма, текст уведомления или заголовок отчёта через админ-панель. Сама идея не плоха — проблема в том, что возможность «редактировать» часто путают с «рендерить». Даже если текст можно вводить через админ-панель, это не означает, что он должен автоматически превращаться в язык шаблонов. Если в админ-панели есть поле «шаблон», которое потом обрабатывается через Blade::render(), это именно эта ошибка — просто с предположением, что её использует только администратор. Но если аккаунт администратора скомпрометирован или есть внутренняя угроза, через этот же путь можно получить полный контроль над сервером. Поэтому предположение «сюда заходит только доверенный пользователь» само по себе недостаточная защита.

Проверка после исправления

  • Отправлены ли пробы вида {{7*7}} во все места, где текст может использоваться как шаблон, и остался ли ответ неизменённым
  • Не передаётся ли текст пользователя напрямую в такие функции, как Blade::render() или Twig::createTemplate()
  • Используется ли песочница или ограниченная замена placeholder'ов там, где динамический шаблон действительно нужен
  • Оставлен ли «сырой» вывод только для доверенных источников

Итог

SSTI не так широко известна, как SQL-инъекция, но последствия сопоставимы — часто это полный контроль над сервером. Правило простое: передавайте в шаблонизатор только шаблоны, написанные разработчиком, а текст пользователя — только как данные. Этот тип инъекций также входит в список OWASP Top 10.

Хотите узнать, есть ли в вашем приложении SSTI или другие инъекционные уязвимости? Оставьте заявку на сканирование — Claude AI агент просмотрит вашу кодовую базу, а наш специалист подтвердит находки и поможет их закрыть.