Инъекции в шаблоны (SSTI)
Понимаем, как пользовательский ввод, попавший в шаблонизатор, превращается в исполняемый код, и как этого не допустить.
SSTI (Server-Side Template Injection) — уязвимость, при которой ввод пользователя попадает не в данные шаблона, а в сам текст шаблона, и движок исполняет его как выражение.
Шаблонизатор (Jinja2, Twig, Freemarker, ERB и другие) — это движок, который берёт шаблон с плейсхолдерами и подставляет в них данные. Нормальный поток такой: разработчик пишет шаблон, а данные передаёт отдельно, как значения. SSTI возникает, когда разработчик по ошибке склеивает пользовательский ввод с текстом шаблона, а затем компилирует результат. Тогда движок воспринимает ввод как часть шаблонного языка — а шаблонные языки умеют вычислять выражения и обращаться к свойствам объектов. Это относится к категории A03: Injection в OWASP Top 10.
Как и любую инъекцию, SSTI мы разбираем, чтобы закрывать её в своём коде. Цель — увидеть опасный паттерн и заменить его на безопасный, а не атаковать чужие приложения (это статья 272 УК РФ).
Зачем это знать защитнику
SSTI коварна тем, что маскируется под «обычную подстановку строки». Разработчик хочет сделать персональное приветствие или письмо с именем пользователя и неосторожно вставляет имя прямо в шаблон. Внешне ничего не выдаёт уязвимость — пока кто-то не пришлёт вместо имени шаблонное выражение. Понимая механику, вы держите границу «шаблон — это код, данные передаются отдельно» и не ставите ввод на сторону кода.
Как возникает уязвимость
Сравним два способа использовать шаблонизатор. Безопасный — когда шаблон фиксирован, а ввод приходит как значение переменной. Уязвимый — когда ввод встраивается в текст шаблона перед компиляцией.
# УЯЗВИМО: ввод склеивается в ТЕКСТ шаблона, потом компилируется
from jinja2 import Template
def greet(name):
# name пришёл от пользователя и стал частью исходника шаблона
tpl_source = "<p>Привет, " + name + "!</p>"
return Template(tpl_source).render() # движок исполнит всё, что внутри {{ ... }}
Пока name — обычная строка, всё работает. Но если в name приходит конструкция шаблонного языка вида {{ выражение }}, движок не выведет её как текст — он её вычислит. Например, ввод {{ 7 * 7 }} вернёт 49 — это контрольный признак, по которому уязвимость обнаруживают на учебном стенде: число «оживает» вместо того, чтобы вывестись буквально. Дальше выражение можно усложнять и через свойства объектов добираться до опасных вызовов. Конкретные «оружейные» цепочки мы здесь не приводим — важен принцип: ввод оказался кодом.
Как это работает под капотом
Шаблонный язык — это полноценный мини-язык: у него есть выражения, доступ к атрибутам объектов, иногда вызовы. Когда движок компилирует шаблон, он превращает {{ ... }} в исполняемый код. Если внутри этих скобок оказался пользовательский ввод, исполнится пользовательский ввод. Через цепочку обращений к встроенным атрибутам объектов (родительские классы, глобальные функции окружения) атакующий способен дотянуться до выполнения команд. Поэтому даже «песочница» шаблонизатора — лишь дополнительный барьер, а не основная защита: основная защита в том, чтобы ввод вообще не попадал в текст шаблона.
Как защититься
1. Никогда не подставляйте ввод в текст шаблона
Шаблон должен быть статическим, написанным разработчиком. Пользовательские данные передавайте как значения переменных в контекст рендера — движок выведет их как обычный текст и не станет вычислять.
# БЕЗОПАСНО: шаблон фиксирован, ввод приходит как ДАННЫЕ
from jinja2 import Template
GREETING = Template("<p>Привет, {{ name }}!</p>") # шаблон задан заранее
def greet(name):
return GREETING.render(name=name) # name подставится как значение, не как код
Вывод:
greet("Аня") -> <p>Привет, Аня!</p>
greet("{{ 7 * 7 }}") -> <p>Привет, {{ 7 * 7 }}!</p> (выведено буквально, не вычислено)
2. Включайте автоэкранирование вывода
Чтобы значения переменных не приводили заодно к XSS, держите включённым автоэкранирование (autoescape) — движок сам превратит <, >, кавычки в HTML-сущности. Это защищает от внедрения HTML/JS через данные, но помните: автоэкранирование не спасает от SSTI, если вы склеили ввод в сам шаблон — там оно применяется уже после компиляции.
3. Песочница — дополнительный барьер
Если по требованиям пользователь всё же должен присылать шаблон (например, кастомные письма), используйте песочничный режим движка (sandboxed environment), который запрещает доступ к опасным атрибутам и вызовам. Это снижает риск, но не отменяет правила «доверенный шаблон лучше любой песочницы». Лучше дать пользователю ограниченный набор плейсхолдеров, а не полноценный шаблонный язык.
4. Разделяйте «движок шаблонов» и «движок логики»
Для пользовательского контента берите простые подстановочные форматы (только именованные плейсхолдеры без выражений), а полноценный шаблонизатор оставляйте для шаблонов, которые пишете вы. Если пользователю нужно влиять на вид письма или страницы, дайте ему набор готовых полей-плейсхолдеров (имя, дата, сумма), а не возможность писать выражения. Так пространство ввода ограничено данными и в принципе не пересекается с языком шаблона.
5. Как обнаружить SSTI
На собственном стенде уязвимость находят простым контрольным признаком: если в поле, которое потом попадает в страницу, подставить безобидное арифметическое выражение шаблонного синтаксиса, и в ответе вместо буквального текста появляется вычисленный результат — значит, ввод трактуется как код. В продакшене помогает обратное наблюдение: логируйте и расследуйте случаи, когда в пользовательских данных встречаются последовательности шаблонного синтаксиса (двойные фигурные скобки, управляющие конструкции движка) — для большинства полей это аномалия. Code review с конкретным вопросом «не склеивается ли где-то ввод в исходник шаблона?» закрывает большинство случаев ещё до релиза.
Итоги
- SSTI возникает, когда ввод попадает в текст шаблона, а не в данные, и движок вычисляет его как выражение.
- Признак на стенде: выражение вроде
{{ 7 * 7 }}возвращает49вместо буквального вывода. - Главная защита — статический шаблон плюс передача ввода как значения переменной в контекст рендера.
- Автоэкранирование защищает от XSS в данных, но не от SSTI при склейке; песочница движка — лишь дополнительный барьер.
- Тестировать инъекции можно только на своих системах и в разрешённой лаборатории (УК РФ ст. 272).