模板引擎本是为了让页面更干净:{``{ user.name }} 渲染一下,活完就走。麻烦始于有人把用户输入当成模板来渲染 ------搜索关键字、错误消息、邮件主题、运营配置里的"自定义欢迎语",直接丢进 render_template_string(user_input)。于是攻击者写入的不再是数据,而是模板语法;在服务端一执行,就成了 SSTI(Server-Side Template Injection,服务端模板注入)。
Jinja2 是 Python Web 生态里极常见的引擎,Flask/Django 相关生态、各类工具站都能撞见。它自带 Sandbox(沙箱)机制,专门限制模板里能碰的属性和调用。于是攻防叙事常常变成两截:
- 先证明 SSTI 存在;
- 若开启了沙箱,再谈沙箱逃逸能否摸到 RCE。
一、SSTI 到底注入了什么
1. 和 XSS 的差别
XSS 在浏览器 里执行脚本;SSTI 在服务器 上让模板引擎执行模板逻辑。
危害上限通常是服务端 RCE、读配置、读密钥,比"弹个窗"重一个数量级。
2. 和命令注入的差别
命令注入进的是 shell;SSTI 进的是模板求值器。最终都可能摸到 os.system 一类能力,路径不同。
3. 一句成因
不可信字符串,被当成模板源码编译/渲染了。
安全的写法永远是:模板固定,用户数据只当变量代入。
text
危险:render(user_input)
安全:render(fixed_template, name=user_input)
二、Jinja2 在实战里怎么中招
1. 典型危险 API
jinja2.Template(user_input).render(...)Environment.from_string(user_input)- Flask:
render_template_string(user_input) - 任何"把用户字符串当模板"的封装
业务借口常见:邮件模板自定义、报表标题公式、多租户主题、错误页回显表达式。
2. 为什么开发觉得没事
"我们只允许双花括号里写变量名""我们开了沙箱""这是内部运营功能"。
内部运营账号被钓鱼、后台存在越权时,内部功能一样打穿主机。沙箱也不是数学证明级隔离(见后文)。
3. 识别指纹
黑盒:在参数里塞模板表达式,看是否被求值。
例如提交数学表达式风格的模板语法,若回显计算结果而不是原文,就高度可疑。
不同引擎语法不同(Jinja2、Twig、Freemarker、Velocity...),先认引擎再测。
白盒:全局搜 render_template_string、from_string、Template(。
三、无沙箱时的危害直觉
在未沙箱或沙箱可绕的环境,模板里往往能通过对象图走到 Python 内置能力,进而读文件、执行命令。
具体属性和链式调用随版本变化,公开靶场教程里有大量历史写法------防御方应假定:模板=代码,而不是去背哪一条链今天还能用。
授权测试证明到"能执行服务端表达式并触及敏感能力"即可定高危;不必默认上线反弹 shell。
四、Jinja2 沙箱:它到底防了什么
1. SandboxedEnvironment
Jinja2 提供沙箱环境,拦截模板中被认为不安全的属性访问与方法调用(例如尝试摸某些危险下划线属性、某些内置函数路径)。
意图是:允许美工/运营写有限表达式,不允许一脚踢进 os。
2. 沙箱不是虚拟机
务必建立正确预期:
- 它是模板引擎层的策略拦截,不是硬件隔离;
- 策略依赖"黑名单/规则"思维,历史上多次被绕过研究;
- 业务若把大量对象塞进模板上下文,等于给攻击者更大的对象图;
- 沙箱配置被改松、自定义过滤器不安全,等于没沙箱。
因此:"我们开了沙箱"不能写进风险接受的唯一理由。 正确理由只能是:"用户输入永不作为模板源码。"
3. 沙箱逃逸在攻防里指什么
指:在 SandboxedEnvironment 下,仍通过属性穿越、替代路径、可调用对象、格式化串、已知缺陷等,触达本被禁止的能力,最终往往是 RCE 或读文件。
研究社区长期关注这类问题;版本升级会修一截,新绕过也可能再出现。对企业而言,追逃逸技巧的 ROI 远低于消灭危险渲染入口。
五、"实战"应有的姿势:授权测试流程
1. 确认引擎与入口
- 参数是否进了模板字符串;
- 是 Jinja2 还是其它;
- 是否
SandboxedEnvironment; - 上下文塞了哪些全局对象。
2. 确认 SSTI
用无害表达式证明求值(例如简单运算或输出固定字符),避免一开始就走攻击性链。
记录:请求位置、回显位置、是否异步渲染(邮件类可能延迟)。
3. 评估沙箱
- 未开沙箱:按严重 RCE 候选;
- 开了沙箱:证明仍可做敏感操作再定级;若仅能有限输出,写清"受限但仍危险(信息泄漏/逻辑绕过)";
- 不要在客户生产盲打破坏性逃逸。
4. 报告写法
写清危险 API 位置、业务功能、复现用的最小无害证明 、修复建议(见下)。
逃逸细节够支撑定级即可,避免把报告写成公开 exploit 教程附件。
六、防御:比研究逃逸更重要的工程现实
1. 根治
永远不要让用户输入成为模板源码。
运营要"可配置模板",用白名单变量的固定模板 + 可视化块,而不是开放任意 Jinja 语法。
2. 必须动态时
- 仅
SandboxedEnvironment仍不够; - 严格限制上下文,不传 request、config、敏感模块;
- 禁止用户自定义过滤器/测试器指向危险函数;
- 最好改用面向非技术人员的模板语言子集,或静态模板 + 替换。
3. 架构替代
邮件内容:MJML/固定布局 + 变量替换(字符串 replace 也要防二次注入进 HTML,那是 XSS 议题)。
报表:专用表达式引擎,运算符白名单,不暴露 Python 对象。
4. 版本与依赖
保持 Jinja2/Flask 等更新;关注安全公告。沙箱修复依赖升级。
5. 权限与纵深
应用最小权限;密钥不进模板上下文;容器只读;出站限制------即使 SSTI 发生,少丢一点。
6. 检测
- 代码扫描:
render_template_string、from_string; - WAF:模板语法特征(辅,易误报漏报);
- RASP/运行时:模板渲染后异常子进程;
- 日志:运营模板发布需审计。
七、和其它漏洞的组合
- XSS:模板注入若输出进 HTML,可能同时埋 XSS;优先服务端 RCE 视角。
- 沙箱逃逸失败仍可读配置:信息泄漏也能导致下一步攻击。
- 后台 SSTI + 弱口令/CSRF:组合拳。
- SSTI vs 客户端模板:Angular/Vue 客户端模板注入是另一类,别写错报告标题。
八、Jinja2 安全编码清单(可贴 Wiki)
- 禁止
render_template_string处理用户/运营原始输入 - 模板文件只来自仓库,不来自数据库"任意字符串"
- 上下文不传入敏感对象
- 若使用沙箱,仍做入口评审并保持版本更新
- 邮件/短信模板走白名单变量
- CI 扫描危险 API
九、学习路径(靶场向)
- 自建最小 Flask 应用:故意
render_template_string用户输入,观察表达式求值; - 改成固定模板 + 变量,确认"注入"消失;
- 打开
SandboxedEnvironment,理解拦截现象; - 阅读 Jinja2 官方沙箱文档与安全更新说明,建立"沙箱会变、入口才是根"的观念;
- 在公司代码库搜危险 API,出整改列表。
把第 2 步做扎实,比收集一百条逃逸表达式更接近"实战精通"。
十、深度讨论:为什么沙箱逃逸会反复成为专题
因为产品需求与安全模型冲突:
- 产品想把"可编程模板"交给运营;
- 引擎想在同一进程里执行表达式;
- 沙箱想用策略模拟隔离。
这三者很难同时完美。每次逃逸研究突破,本质是指出:策略隔离在图灵完备或对象暴露过大时不可靠。
企业决策应是:
放弃在服务端向不可信用户提供通用模板编程能力;
需要可编程时,放到隔离的 Worker、无秘密环境、强审计,而不是放在主 Web 进程里碰生产密钥。
这才是"沙箱逃逸实战"对架构师真正有用的结论。
十一、授权测试中的定级建议
| 情况 | 建议定级 |
|---|---|
| 用户输入当模板,无沙箱,可执行系统命令 | 严重 |
| 无沙箱,可读密钥/配置 | 严重/高危 |
| 有沙箱,仍证明可逃逸至命令执行 | 严重 |
| 有沙箱,仅有限文件读或信息泄漏 | 高危(视数据) |
| 仅自 XSS 式模板字符回显但未求值 | 可能不是 SSTI |
定级写清证据,避免把客户端模板误报成 SSTI。
十二、收尾
SSTI 的第一课是:模板引擎是代码执行器。
Jinja2 沙箱的第一课是:它能提高成本,不能从根上把"用户模板"变成安全功能。
所谓沙箱逃逸实战,对红队是条件与版本的博弈;对蓝队与开发,是别把战场摆出来。
请记住这句比任何 payload 都长寿的话:
固定模板,变量代入;
用户输入不是模板。
沙箱是保险绳,不是悬崖边的护栏替代品。
今晚若只做一件事:在代码库搜 render_template_string 与 from_string,看参数是否来自请求或数据库"模板字段"。
若有,能改掉就改掉;改不掉就降权限、加沙箱、加审计,并列入风险台账------而不是默念"我们有沙箱"然后睡死。
附:危险与安全对照
| 做法 | 评价 |
|---|---|
render_template_string(request.args['q']) |
危险 |
render_template('x.html', q=request.args['q']) |
正确方向 |
| DB 存任意 Jinja 给用户改 | 高风险产品形态 |
| DB 存白名单变量 + 固定模板 ID | 可接受 |
| 仅靠 SandboxedEnvironment | 缓解,非根治 |