模板注入漏洞根因是用户可控输入参与模板内容生成或加载,如template.compile(userinput)或new template(stringreader(userinput));默认html转义仅防xss,无法阻止${input?eval}等引擎级代码执行。

模板渲染引擎本身不是漏洞,问题出在「谁控制了模板内容」和「模板怎么加载」。只要模板字符串来自用户输入、拼接或不可信来源,就大概率存在模板注入;而默认转义只防XSS,不拦${userInput?eval}这类执行。
怎么判断模板内容是否被用户可控
关键不是看用了什么引擎,而是看template.compile()、new Template()、res.render()这些调用的参数从哪来:
- 参数是硬编码字符串或磁盘文件路径(如
'./templates/index.ftl')→ 安全 - 参数含
userInput、req.query.tpl、StringReader(userInput)→ 高危 - 模板里用了
{{@data}}(art-template)或${data?eval}(FreeMarker),且data来自请求→ 危险信号 -
include或import指令中路径拼接了req.params.name→ 目录遍历+任意文件读取
FreeMarker 和 art-template 的危险配置检查点
防护不在业务层,而在初始化时。漏掉这几行,后面再严谨也白搭:
- FreeMarker:
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER)必须设,否则?eval可加载任意类 - FreeMarker:
cfg.setClassicCompatible(false)关掉旧语法兼容,避免绕过限制 - art-template:
template.defaults.compileDebug = false防止模板编译错误泄露路径 - art-template:
template.defaults.cache = true启用缓存,避免每次请求都重新编译不可信模板字符串
为什么 htmlspecialchars() 对模板注入完全无效
htmlspecialchars()只处理输出阶段的 HTML 字符转义,它把<script></script>变成文字,但对模板引擎内部的语法解析毫无影响:
- 传入
template.compile('<div>${' + userInput + '}</div>'),引擎先解析${...}为表达式,再执行——此时userInput已是 JS 代码上下文 - 哪怕你对最终渲染结果再做一次
htmlspecialchars(),也挡不住模板阶段已执行的fetch('/api/admin') - 真正有效的过滤,必须在模板字符串生成前:拒绝拼接、强制白名单路径、禁用
?eval等高危内建函数
富文本 + 模板渲染的双重陷阱
当富文本字段(如 CMS 编辑器内容)被直接塞进模板变量,又没走净化流程,就会触发链式漏洞:
- 入库前未用
HTMLPurifier::clean()→ 数据库存了<img src="x" onerror="alert(1)"> - 渲染时模板写成
<div>{{content}}</div>→ art-template 默认转义,安全 - 但若某次改成
<div>{{@content}}</div>→ 绕过转义,原样插入 DOM → XSS - 更糟的是,若
content含<script>fetch('/admin/delete')</script>,且模板引擎支持执行,就是模板注入 - 所以富文本字段绝不能直接进模板编译,必须先过
HTMLPurifier,再进{{content}},且禁用所有表达式插值
最常被忽略的一点:模板引擎的「加载」动作本身可能被劫持。比如 Node.js 中用fs.readFileSync(userInput + '.ftl')加载模板,攻击者传../../../etc/passwd就能读敏感文件——这根本不是模板语法问题,而是路径拼接失控。排查时得顺着compile、render、include向上溯源,盯死第一行参数来源。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











