模板注入怎么发生
SSTI 出现在「用户输入被当作模板内容渲染」的时候:服务端模板引擎把输入编译成模板,注入的表达式就按引擎能力被求值,可以读取变量、访问对象属性,严重时执行系统命令。根因是把数据和模板混在一起。正确做法是区分两者:模板内容只来自代码仓库,用户输入一律作为变量参数传入。
基本信息
| 常见引擎 | Jinja2(Python)、Twig(PHP)、Freemarker 与 Velocity(Java)、Thymeleaf(Java) |
|---|---|
| 典型成因 | render_template_string(用户输入)、模板名或模板路径可控、模板字符串拼接 |
| 常见入口 | 站内信与通知模板、报表导出、邮件模板、个性化页面、错误页 |
| 防护方向 | 固定模板文件加变量参数,模板内容与请求数据彻底分离 |
| 加固手段 | 沙箱环境、表达式能力白名单、模板来源限制 |
| 验证方式 | 授权环境提交简单算术表达式,观察是否被求值 |
引擎与危险入口
| 引擎 | 语言 | 危险入口 |
|---|---|---|
| Jinja2 | Python | render_template_string、模板名拼接 |
| Twig | PHP | 动态模板加载、未启用 sandbox 扩展 |
| Freemarker | Java | 模板字符串拼接、new() 实例化能力 |
| Velocity | Java | 允许用户编辑模板内容 |
| Thymeleaf | Java | 片段表达式、模板前缀可控 |
| Smarty | PHP | 模板内容可由用户提交 |
风险与安全对比(Jinja2)
风险与安全对比(Jinja2)
# 风险:把用户输入当模板内容渲染
# return render_template_string('Hello ' + name)
# 风险:模板名由请求决定
# return render_template(request.args['tpl'])
# 安全:模板始终来自代码仓库,用户输入只作为变量
return render_template('hello.html', name=name)
# 需要用户自定义展示时,用受限占位符替换,不暴露表达式能力加固要点
加固要点
沙箱环境:Jinja2 用 SandboxedEnvironment,Twig 启用 sandbox 扩展
能力收敛:禁用危险过滤器与全局函数,只放行白名单
模板来源:仅允许仓库内固定文件,拒绝请求传入路径或模板名
审计:全量检索 render_template_string 与动态模板加载调用点典型使用场景
- 站内信、通知中心支持模板配置时的安全评审
- 报表导出功能支持自定义表头与计算列
- 运营后台可编辑邮件模板,需要确认渲染边界
- 渗透测试报告中 SSTI 类问题的整改与复核
- 老系统排查模板字符串拼接的历史写法
- 引擎与沙箱版本升级后关注已知逃逸手段
需要用户自定义展示内容时,使用受限占位符替换(如只允许姓名、单号等白名单字段),而不是把模板引擎能力直接暴露给用户。
沙箱不是万能解,多种模板引擎历史上都出现过沙箱逃逸。最终还是要保证用户输入不进入模板层,沙箱只作为纵深防护。