Найдите серверную инъекцию в шаблон (SSTI) в lodash на пользовательском вводе
Этот код на Node.js собирает строку шаблона lodash из пользовательского name и затем рендерит её. Определите уязвимость и как она доходит до выполнения кода, затем опишите исправление.
Ограничения:
nameконтролируется атакующим;escapeHTMLэкранирует только спецсимволы HTML- lodash
_.templateкомпилирует строку в функцию и выполняет встроенные${...}
const compiled = _.template("Hello " + escapeHTML(name) + ".");
const html = compiled(data);
Определите причину.
Серверная инъекция в шаблон (SSTI) — name становится частью исходника шаблона, а escapeHTML нейтрализует только HTML, не синтаксис шаблона. Выражение ${...} компилируется и исполняется на сервере, давая RCE. Фикс: передавать name как данные в фиксированный шаблон, а не в текст.
- ✗Считать escapeHTML защитой от инъекции синтаксиса шаблона
- ✗Путать SSTI с отражённым XSS (выполнение на сервере, не в браузере)
- ✗Собирать строку шаблона из пользовательского ввода
- →Почему экранирование HTML не мешает выполнению выражения ${...}?
- →В чём разница между передачей ввода как данных и как текста шаблона?
Уязвимость
name склеивается в исходник шаблона, а не передаётся как данные:
const compiled = _.template("Hello " + escapeHTML(name) + ".");
escapeHTML экранирует <, >, &, но не трогает синтаксис шаблона ${...}. Поэтому любое выражение ${...} во вводе доживает до компиляции и выполняется на сервере с правами процесса — это SSTI, ведущий к RCE. Признак в коде: строка шаблона собирается конкатенацией с пользовательским значением; экранирование вывода здесь не помогает, потому что оно защищает браузер, а не компилятор шаблона.
Исправление
Передавайте ввод как данные в фиксированный шаблон, тогда он никогда не интерпретируется как код:
const tpl = _.template("Hello <%- n %>.");
const html = tpl({ n: name }); // name — данные, не текст шаблона
✅ Текст шаблона теперь константа; <%- %> безопасно подставляет name как значение. ⚠️ Любая склейка ввода в строку шаблона возвращает уязвимость.