模板注入漏洞(SSTI)的判定核心是用户输入是否被当作模板语法执行,而非仅作数据渲染;关键看模板字符串是否来自运行时拼接(如new Template(userInput)、template.compile(userInput))、是否含${data?eval}等危险表达式,以及引擎初始化是否启用SAFER_RESOLVER等安全配置。

模板注入漏洞(SSTI)不是靠“看有没有script标签”来判断的,而是看用户输入是否被当作模板语法执行——只要它进了Template、compile()、render()这类函数的**模板内容参数**,就危险了。
怎么确认模板内容来自用户输入
关键不是渲染逻辑写了什么,而是模板字符串本身从哪来。常见高危模式:
-
new Template(userInput)或new Template('xxx', new StringReader(userInput))(FreeMarker) -
template.compile(userInput)(art-template),且userInput是拼接或 HTTP 参数读取的 -
res.render('index', { data: userInput })+ 模板里写了${data}但没转义,且引擎开启了表达式解析(如 FreeMarker 的?eval) -
include或import路径含req.query.tpl等可控参数,比如<#include file="${tpl}.ftl}">
一旦发现模板内容是运行时拼出来的,而不是从磁盘固定路径加载的文件,基本可以判定存在注入风险。
为什么{{data}}转义不等于安全
HTML 转义只防 XSS,不防 SSTI。它把<script></script>变成文字,但挡不住${7*7}这种计算表达式被执行。
立即学习“前端免费学习笔记(深入)”;
-
{{data}}(默认转义)→ 安全,仅输出 -
{{= data}}或{{@data}}(art-template)→ 危险,直接执行 JS -
${data?eval}或#{data}(FreeMarker)→ 危险,强制解析为模板语法 -
<#assign x = data?eval>→ 更隐蔽,但一样危险
这些语法一旦出现在用户可控上下文中,就是执行入口。审查时必须逐行盯住模板语法调用点,不能只看变量名。
FreeMarker 和 art-template 的初始化配置检查点
防护不在业务层,而在引擎初始化那一刻。漏掉这一步,后面所有过滤都白搭。
- FreeMarker:
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER)必须设,否则?api、?eval可加载任意类 - FreeMarker:
cfg.setClassicCompatible(false)关闭旧版兼容模式,避免绕过限制 - art-template:
template.defaults.open = '{{'和template.defaults.close = '}}'不能被动态覆盖,否则可能引入非预期语法 - art-template:
template.defaults.compileDebug = false防止调试信息泄露模板路径和结构
这些配置通常只在应用启动时设一次。如果代码里有多个new Configuration()或template.defaults = {...}赋值,就要逐个确认是否都配对了安全选项。
快速验证是否存在 SSTI 的最小测试方法
别一上来就试cat /etc/passwd,先确认基础执行能力。在疑似模板渲染处提交以下 payload,观察响应:
- FreeMarker:
${7*7}→ 返回49即存在基础 SSTI - art-template:
{{7*7}}→ 同样返回49 - 通用试探:
${"abc".length}、{{"abc".length}}→ 看是否解析字符串方法 - 注意:某些引擎会静默失败或报错,需结合 HTTP 状态码(如 500)和错误消息判断
真正麻烦的是那些“看起来没回显”的场景——比如模板内容被写入日志、邮件或异步任务。这时候得顺着数据流往下游追,而不是只盯着 HTTP 响应体。



















