邮件模板注入漏洞的本质是用户输入被当作模板语法执行,如昵称“admin${1+1}”拼入FreeMarker字符串后渲染出“admin2”,表明表达式已被求值;其根源在于模板内容运行时拼接而非文件加载,且未禁用?eval、{{= data}}等危险语法,HTML转义无法防御,因表达式在转义前即被执行。

邮件模板注入漏洞不是“模板写得不漂亮”,而是用户输入被当作模板语法执行——比如把昵称 admin${1+1} 直接拼进 FreeMarker 字符串,结果渲染出 admin2,说明表达式已被求值。这种漏洞一旦触发,攻击者就能读取服务器环境变量、遍历文件系统,甚至执行命令。
怎么确认模板内容是运行时拼接的,而不是从文件加载
所有危险都始于「模板来源不可信」。只要模板字符串含用户输入,就踩了红线。
- 查
new Template("name", new StringReader(userInput))或cfg.getTemplate(null, new StringReader(...))—— 这类构造器调用等于把用户输入直接喂给引擎 - 查
template.compile(userInputString)(art-template)或engine.createTemplate(userInput)(Handlebars)—— 动态编译字符串比加载文件路径危险十倍 - 查模板路径是否含变量:
res.render(userControlledPath + ".ftl", data)或include "${userInput}.html"—— 这既是模板注入,也是路径遍历
FreeMarker 和 art-template 里哪些语法必须禁用
引擎自带的“动态执行”能力,就是攻击者的后门。不能靠业务层过滤,得在配置层面关死。
- FreeMarker:禁用
?eval、?interpret、#include和#import中的用户可控路径;检查cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER)是否生效 - art-template:禁用
{{= data}}(JS 表达式)和{{@data}}(绕过转义);确认template.defaults.open === "{{"且未被运行时覆盖 - 通用红线:任何模板中出现
${...}、#{...}、{{...}}且其内部变量来自用户输入,都必须加白名单校验或彻底移除
为什么 HTML 转义(如 {{data}})防不住模板注入
默认转义只处理输出渲染,不阻止语法解析。它能把 <script></script> 变成纯文本,但对 ${userInput?eval} 完全无效——因为表达式在转义前就被执行了。
立即学习“前端免费学习笔记(深入)”;
- 场景对比:
res.render("email.ftl", { subject: userInput })模板里写<title>${subject}</title>→ 危险;若写<title>${subject?html}</title>→ 仅防 XSS,不防${subject?eval} - 真正安全的做法是:用户输入只作为数据传入,绝不参与模板结构生成。昵称就该是
data.nickname = "admin",而不是templateStr = "...${"admin"}..." - 测试方法:往输入里塞
${1+1}或${request?api_version},看邮件正文是否出现计算结果或敏感信息
最易被忽略的一点:邮件服务常复用 Web 模板引擎,但没同步更新配置。比如 Web 层用了 SAFER_RESOLVER,邮件模块却用独立 Configuration 实例且仍设为 UNRESTRICTED_RESOLVER——漏洞就藏在这行没被 review 的初始化代码里。



















