news 2026/10/13 3:51:44

三大模板引擎SSTI沙箱逃逸深度解析与防御实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
三大模板引擎SSTI沙箱逃逸深度解析与防御实践

被派到现场做授权评估,目标是一个用Jinja2渲染页面内容的业务系统。模板内容可控,数据交互也很正常,标准的SSTI检测点。我先丢了一个最常见的探测串{{ 7*7 }},页面直接输出了49。再丢{{ ''.__class__.__mro__ }},拦截提示马上弹了出来。目标开了沙箱。

这个场景这两年越来越常见。很多开发团队已经知道不能直接拼接模板,SSTI漏洞的曝光度也上来了,于是大家开始用各种沙箱机制来"兜底"。但从我实际测过的项目来看,沙箱在多数场景下只能提高利用门槛,并不能从根本上拦住一个真正理解模板引擎底层机制的测试者。模板引擎本质上是一套与宿主语言共享对象模型的DSL,它的沙箱是策略层、配置层的限制,而不是语言级的隔离边界。

这篇文章我会把Jinja2、Twig、Freemarker这三个主流模板引擎的沙箱逃逸思路完整拆开讲,包括它们各自的沙箱实现方式、核心逃逸逻辑、版本差异,以及最后如何从攻击视角反推一套真正能落地的防御方案。内容适合已经接触过SSTI、想往深处走的安全测试者,也适合正在给模板渲染层做加固的开发者。

1. 沙箱存在的意义:模板引擎到底在防什么

1.1 先理清SSTI的本质

SSTI之所以危险,是因为模板并不是简单的"字符串拼接工具"。模板在渲染前会被编译成宿主语言代码(Python、PHP、Java),模板里的表达式、标签、过滤器最终都会进入执行流程。当用户输入能够影响模板的源码结构时,输入就成了代码的一部分。

关键在于理解模板引擎的分层模型。拿Jinja2举例,{{ user.name }}会被编译成对上下文对象user的属性访问,最终变成Python的getattr(user, 'name')。这意味着模板表达式天然具备对象操作能力。Jinja2在渲染时能访问的对象、内置过滤器、全局函数,都属于模板的"攻击面"。

沙箱瞄准的就是这个攻击面。沙箱的职责不是阻止所有代码执行,而是在"模板必须能访问对象属性和调用方法"和"不能访问os.system这类危险入口"之间画一条可配置的线。这条线靠什么实现?靠拦截器、白名单、名称过滤,而不是靠隔离进程或关闭解释器。这就是沙箱可以被穿透的根本原因。

1.2 三种引擎的沙箱定位差别

三种引擎对沙箱的定位很不一样,这也是很多人用起来感觉差异巨大的根源。

引擎沙箱机制默认安全级别本质限制方式
Jinja2SandboxedEnvironment较为严格属性名过滤、is_safe_callable检查
TwigSandboxExtension + SecurityPolicy默认不启用,启用后看配置白名单方法、过滤器、标签、函数
FreemarkerNewObject策略 + TemplateClassResolver中低限制new对象的类解析

Jinja2的沙箱偏向"黑名单+安全检查":默认阻止下划线开头的属性访问,维护一份"已知安全"的类型清单。Twig的沙箱偏向"白名单":只有显式允许的tags、filters、functions、methods才能用。Freemarker的沙箱则只盯着一个关键的入口——?new()能否实例化危险类,对其他漏洞点相对宽松。

这三套机制各自的薄弱环节完全不同。Jinja2的问题在于属性名过滤能不能覆盖所有等价访问;Twig的问题在于白名单配置的完整性和回调型过滤器的出口;Freemarker的问题在于Java反射链太丰富,类解析即使被限制,仍然存在其他拿到执行能力的路。接下来逐个拆。

2. Jinja2逃逸思路:从内省链到filter链

2.1 SandboxedEnvironment的检查逻辑

Jinja2的沙箱主体是sandbox.py里的SandboxedEnvironment。它重写了_getattr方法,逻辑并不复杂:如果属性名以_开头,直接拒绝;如果是_getattr内部信任的安全类型,如str、int、list这种基础设施类型,放行;其余情况交给is_safe_callable判断。

这个模型最典型的问题就是"属性访问的等价写法"。Jinja2模板语法里,user.name和user['name']都会走getattr,但沙箱只拦以_开头的属性名。在早期的沙箱实现里,attr过滤器并没有做同样的名称前缀检查,所以{{ ''.__class__ }}被拦,但{{ ''|attr("__class__") }}能直接打穿。

官方在后续版本里给attr补上了对__前缀的检查,我在本地验证环境中测试过,Jinja2 3.1.4之后|attr("__globals__")这类写法在沙箱下同样会触发拦截。早期版本里这个口子非常稳定,很多老系统的漏洞报告都出自这里。

2.2 内省链:拿到危险对象的几种路径

绕过沙箱拿到os模块或者__import__,核心依赖Python对象的内省机制。链路的起点一般是str子类实例,因为模板上下文里字符串最容易被我们控制。

{{ ''.__class__.__mro__[1].__subclasses__() }}

这段如果在沙箱下直接用,会被属性名拦截直接掐死。但可以借助attr过滤器改写(注意版本差异,新版已修复):

{{ ''|attr('__class__')|attr('__mro__') }}

拿到__subclasses__()后,老套路是逐个查找包含os模块引用的类。Python标准库中能间接拿到__import__的类至少有几十个,我常用的是warnings.catch_warnings下的_module.__builtins__['__import__']。这条路在非沙箱环境下很稳定,但在沙箱下还需要解决一个关键问题:过滤器的参数能不能携带函数名。

这就是filter链的用处。Jinja2的map、select、reject过滤器可以接收一个字符串参数来指定方法名,例如''|attr('__class__')|attr('__mro__')|map('__subclasses__')。字符串作为参数传给过滤器,绕开了沙箱对"属性访问"的检测面。在未完全修复的版本里,这种写法能把__subclasses__拉出来执行。

2.3 format与字符串格式化的另类属性访问

还有一条不被很多人注意的路径:字符串的format方法。Python的格式化字符串语法允许在花括号内访问对象属性:

{{ '{0.__class__}'.format('') }}

格式化字符串内部的.不走getattr的沙箱检查路径,格式化语法自己的解析器会处理属性访问。这在旧版沙箱下能直接绕开属性名拦截。

Jinja2修复这个点的方式也很有意思——不是去改format,而是在沙箱里对格式化字符串本身做黑名单检查。我在测试中注意到,高版本对{0.__class__}这类字符串会直接抛SecurityError,但通过拼接等方式构造格式化串,在某些版本里仍然有操作空间。这说明黑名单方案天然存在覆盖不完整的顽疾。

2.4 版本差异与实测心得

版本区间沙箱绕过难度关键差异
2.x 及 3.0 早期低attr、format 均可绕属性名过滤
3.1.x(补丁后)中attr/format 被修复,需找其他策略缺口
3.1.4+中高属性前缀检查覆盖更全,重点转向配置错误

实测下来,沙箱真正能挡住大部分攻击者的场景,一是版本足够新,二是开发者没有在上下文里暴露额外对象。很多人配置沙箱时忽略了一个问题:SandboxedEnvironment并不能阻止模板访问上下文中已有的对象及其方法。如果业务代码把数据库连接、Redis客户端、甚至某个工具类塞进了模板上下文,那沙箱形同虚设。这类"沙箱没被打穿但上下文泄露直接GG"的案例,我在实际评估里遇到的比纯逃逸链还多。

3. Twig逃逸思路:PHP对象模型下的另类路径

3.1 SecurityPolicy的白名单机制

Twig默认并不启用沙箱,启用后主要通过SandboxExtension配合SecurityPolicy工作。SecurityPolicy管理五个维度的白名单:tags、filters、functions、methods、properties。看起来比Jinja2的黑名单思路严谨,但白名单机制的弱点在于:配置者的疏漏会直接变成漏洞面。

最经典的配置问题是methods白名单。业务需要调用某个对象的getStatus、format这类方法,于是把方法名加进白名单。但PHP的方法解析不区分大小写,而且Twig的方法调用走__call魔术方法时,SecurityPolicy的方法名检查有时会被跳过。某些版本中,白名单配了format,攻击者在模板里调用format的变体写法(如大小写混合)就能绕过。

3.2 回调型过滤器:sort/filter/map的入口

Twig逃逸链里目前最活跃的一支是回调型过滤器。sort、filter、map这几个过滤器支持传入箭头函数作为回调。箭头函数本身是Twig模板语法,但回调参数实际上是PHP闭包,闭包体内可以调用任意"可用"的方法或函数。

思路是这样的:先通过箭头函数拿到一个可执行回调的入口,再在闭包参数上做文章。举个例子,sort过滤器在比较两个对象时,箭头函数被调用,此时如果能触发对象的某个方法,且该方法恰好是__construct这类PHP魔术方法,就有机会把参数传递到底层危险方法。

{{ ['a','b']|sort((a, b) => call(user_func等)) }}这类写法受限于沙箱的函数白名单,所以在严格配置下直接调用call_user_func是会被拦的。但方法的白名单如果包含了__construct,攻击者可以构造一个具有危险构造参数的对象引用,利用排序回调把可控数据注入构造参数。这类路径对PHP版本和应用层的依赖较强,没有绝对通用的payload,但思路是统一的:找一个沙箱允许执行的用户代码入口,把参数精准导向危险函数。

我自己在实际测试中更喜欢先探测沙箱的过滤器白名单,做法是构造{{ 'id'|filter('system') }}这类payload看报错信息。如果报错提示filter不允许,就换map、reduce、sort逐个试。关键是找一个能接收用户函数、且在沙箱白名单内没有覆盖的过滤器。

3.3 方法白名单配置不当时的路子

Twiig沙箱的SecurityPolicy能控制"模板中允许调用的方法",但这个控制粒度很粗。allowed_methods的键值对应的是类名,Twig在运行时判断当前对象是否属于某个允许的类,然后放行该类的所有方法调用。这就带来一个配置悖论:如果业务把某个内部工具类加进白名单,攻击者只要能让模板上下文里出现该工具类的实例,就能调用它的一切公有方法。

我见过一个案例:业务为了在模板中展示日期,把某个日期时间工具类加进了allowed_methods。这个工具类有个setFromArray方法能修改对象属性,顺着这个入口一路调用,最终通过已存在上下文中的__toString、__call走到了可利用的代码面。这类利用依赖具体业务,没法给通用payload,但排查思路很清晰:拿到方法白名单列表,逐个看这些方法的作用,再想如何串联。

3.4 PHP对象的敏感方法面

和Jinja2的环境不同,PHP生态的方法面更宽。call_user_func、system、exec、passthru这类函数在模板侧能不能直接调用,取决于functions白名单;但对象方法不受函数白名单约束,只受methods白名单约束。

如果methods白名单为空或者配置了*(允许所有方法),那模板任意对象的方法调用都放行。这种情况下就算不能直接调用系统函数,只要能拿到一个内部类实例(比如日志类、缓存类、文件处理类),其方法内部通常埋着文件读写或者命令执行的能力。所以Twig沙箱的逃逸,很多时候不是沙箱机制本身有洞,而是白名单配置的覆盖面和它背后允许的对象方法面根本不匹配。

4. Freemarker逃逸思路:Java反射链与内建函数的边界

4.1 模板内建函数与对象模型

Freemarker的模板表达式同样可以访问模型对象的属性和方法,它的内建函数(built-in)体系非常庞大,这是它SSTI攻击面广阔的重要原因。?new、?eval、?string、?js_string这些内建函数各有各的问题面。内建函数在Freemarker中的定位是模板语法的组成部分,沙箱通常不会去禁用它们——因为禁用内建函数等于把模板引擎阉割了。

Freemarker的沙箱逻辑集中在 "到底允许模板创建哪些Java类" 这一点上,其中?new是核心入口。模板里可以写:

<#assign x = "freemarker.template.utility.Execute"?new()>

一旦成功实例化Execute,就能直接执行系统命令。Freemarker历史上最经典的三个可执行工具类:

类名作用
freemarker.template.utility.Execute执行系统命令
freemarker.template.utility.ObjectConstructor实例化任意Java类
freemarker.template.utility.JythonRuntime执行Jython脚本

ObjectConstructor最危险的地方在于能实例化任意Java类,配合Execute或者ScriptEngineManager这类类,命令执行和脚本执行都能覆盖。这些工具类在新版本中被默认限制,但这个默认限制依赖于配置的TemplateClassResolver。

4.2 ?new与TemplateClassResolver的绕过面

Freemarker从某一版本开始引入TemplateClassResolver来校验?new的类名。默认的TemplateClassResolver会拒绝freemarker.template.utility.*包下的类实例化。看起来安全了,但Java反射链的攻击面还是足够深。

常见的思路是绕开freemarker.template.utility这个包名限制,寻找其他能够间接执行代码的类。例如javax.script.ScriptEngineManager,它本身不是freemarker.template.utility包的,通过ObjectConstructor实例化它之后可以加载JavaScript引擎并执行脚本。某些Java版本默认自带Nashorn引擎,这就是一条完整的执行链。

另一个经典绕法是利用freemarker.ext.beans.BeansWrapper及其子类。模板在解析对象属性时实际上依靠BeansWrapper来做JavaBean反射,它允许模板调用Java对象的公有方法。只要能拿到一个叫Class的对象引用(比如某个对象的class属性),理论上可以通过反射拿getRuntime、exec,最终走向命令执行。Freemarker对class属性访问的处理相比Jinja2宽松得多,这是两个引擎对"Java反射"友好度的差异。

4.3 除了?new以外,还有哪些入口

?new被堵死不代表结束。Freemarker的<#assign>配合?eval可以做二次解析;?eval本身不执行Java代码,但它可以递归调用模板表达式,在某些配置下能绕过类解析器的检测面。

还有一条容易被忽略的路:数据模型的入口。Freemarker模板能访问的数据模型(DataModel)中包含的对象,是TemplateHashModel、TemplateMethodModel等Freemarker内置接口的实现。如果模型中有自定义的TemplateMethodModel,那么它的exec方法就是模板侧的代码执行入口。exec方法名在Freemarker内部是约定俗成的接口方法,不会被沙箱拦截,因为沙箱根本不拦接口方法——它只拦类名解析。这个出口在真实业务系统里经常出现:开发者为了方便模板调用,会把一些通用的"工具方法"封装成TemplateMethodModel放进模型,却忽略了这些方法的参数可能来自用户输入。

4.4 Freemarker版本差异的实测感受

版本默认状态逃逸难度
2.3.22及更早?new无限制低
2.3.23起引入TemplateClassResolver中
2.3.30+默认阻止utility包中高

Freemarker还有一个其他引擎没有的特性:它对模板中直接出现的静态类名引用(如freemarker.template.utility.Execute?new())限制较严,但对通过变量间接持有的类对象引用限制较松。Java的class对象是运行时第一公民,一旦某个对象的class属性被模板拿到,后面的展开面就大了。所以Freemarker逃逸的实战思路我一直强调:不要只盯着?new字符串,要想办法在模板上下文里找到已经存在的类对象引用。

5. 三引擎对照:逃逸路径共性分析与防御落地

5.1 一张表看清三引擎的沙箱思路

把三个引擎放在一起对比,能清楚看到它们的设计哲学差异,也能看到共性。

维度Jinja2TwigFreemarker
沙箱默认开启需显式使用SandboxedEnvironment否,需手动启用部分策略默认
限制方式属性名黑名单+安全检查全维度白名单类解析白名单
主要逃逸面filter参数化携带方法名回调过滤器+方法白名单覆盖不全类对象引用+反射
经典入口__class__内省链sort/filter箭头函数?new + TemplateMethodModel
版本落差补丁后难度明显上升依赖配置粒度依赖类解析策略

共性其实很突出:三个引擎的沙箱都试图在"模板语言能力"和"宿主语言对象面"之间划界,但沙箱的判定依据全是名称、签名、白名单这些"语法级"的特征,而不是真正的运行时隔离。只要模板能拿到对象的引用,对象的方法真正能不能被调用,最终还是由宿主语言的动态性决定——Python的__getattr__、PHP的__call、Java的反射,都在绕过沙箱判定后提供了第二层通道。

5.2 从逃逸链反推的共性防御原则

看过这么多逃逸链之后,我得出的防御结论和网上常见的"配置好沙箱就安全"很不一样。沙箱只是最后一层兜底,真正有效的防御应该前移。

第一原则:不可信输入永远不要进入模板结构本身。模板注入的前提是输入能影响模板源码。把用户输入绑定为模板上下文中的一个变量值,即使表达式注入失败,攻击者也拿不到模板语法执行权。这是最根本的解药,沙箱只是缓解。

第二原则:最小化暴露。模板上下文只放渲染所需的最小对象集。能用普通类型(字符串、数字、列表、字典)解决的,绝不塞业务对象。很多系统把Service层、DAO层直接塞进模板上下文,等于把整个业务后门都送给了模板表达式。我在评估中遇到的多数高影响SSTI,靠的其实不是沙箱逃逸,而是上下文里那个可以直接用的工具类。

第三原则:白名单宁可过度收紧也不要先放宽。Twig的SecurityPolicy、Freemarker的类解析器、Jinja2的is_safe_callable,都需要按最小必需原则配置。特别是Twig,默认不启用沙箱的项目就别提防御了;启用后全维度白名单要逐个过一遍,别想当然认为"能跑就行"。

5.3 防御落地清单

结合三种引擎的特点,我给出一份可以直接拿去对照落地的检查清单:

  1. 确认模板引擎版本已升级到最新,三个引擎都在持续修补沙箱绕过,版本落后就没有讨论防御的意义。
  2. 不可信输入一律作为上下文值传入,禁止拼接模板源码。可以在代码评审、CI脚本里加正则扫描,检索Template、render附近的字符串拼接。
  3. Jinja2项目检查是否启用SandboxedEnvironment,并自定义is_safe_callable收紧允许调用的对象类别。
  4. Twig项目检查SandboxExtension是否启用,SecurityPolicy五个维度的白名单逐项核实,禁止*通配。
  5. Freemarker项目检查TemplateClassResolver配置,拒绝freemarker.template.utility.*,同时排查模型中自定义的TemplateMethodModel是否暴露风险方法。
  6. 模板上下文对象清单要纳入资产管理和代码评审,不用的对象及时移除。
  7. 加一层针对SSTI特征的WAF规则:类名探测特征(__class__、__mro__、__subclasses__)、?new、Execute、ObjectConstructor等关键字。WAF不是防御的核心,但能在漏洞被利用时争取响应时间。

5.4 检测侧的思路补充

防御方还需要一个持续检测SSTI的机制。审计模板渲染点的时候,我习惯构造三类探针:数学运算探针(7*7)、属性访问探针(''.__class__、'a'|attr('__class__')、?new)、过滤器/方法面探针('id'|filter('system')、<#assign x='freemarker.template.utility.Execute'?new())。这三类覆盖了模板引擎最吃的语法入口,只要目标返回了运算结果或者对象信息,就说明表达式被解析执行了。

真正的检测难点在闭源系统或者模板内容来自数据库的场景,没有源码审计线索就只能靠黑盒探针。这种情况下我会多试几个编码层级的变形(URL编码、Unicode转义、HTML实体),因为很多系统在存储模板前会做一次解码,底层引擎拿到的是解码后的源。

写在最后的个人体会

沙箱逃逸这件事,我从测试者角度和开发者角度都蹲过。一个很深的感受是:模板沙箱给开发者造成的"安全错觉"比漏洞本身更危险。很多人配好沙箱就松了弦,觉得"反正底层有防护",结果在上下文里塞了一堆业务对象,或者把模板拼接口子留在了某个管理后台。等到出问题,沙箱连拦都来不及拦。

另一个体会是版本管理的重要性。Jinja2的attr绕过、Freemarker的ObjectConstructor链,在不同版本下的可用性差异极大。评估过程中我先看的永远是版本号和配置项,这比背payload有用得多。安全对抗就是这样,不是谁记住的exp多谁就强,而是谁更理解一套机制背后的设计取舍,谁就能在补丁出现时第一时间找到下一条路。

最后分享一个小技巧:在自建验证环境里把三个引擎的沙箱开关都打开,用同一套探针脚本分别打,观察它们的报错信息风格。报错差异就是这个引擎心智能模型的直接体现,看多了之后,黑盒测SSTI时你基本能从报错推断出后端是Python还是PHP还是Java,连指纹识别都省了。这套手感,值得每一个做Web安全的人花一个下午去练。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 3:51:15

数据结构MOOC第二讲线性结构编程题全解析:链表与堆栈实战

去年我把那套经典的数据结构MOOC又从头到尾刷了一遍&#xff0c;编程练习题更是认认真真写完了。这套课配的在线评测平台题目质量很高&#xff0c;尤其是第二讲“线性结构”部分的几道题&#xff0c;看着不难&#xff0c;真正上手写的时候才发现处处是坑。今天专门把第二讲的编…

作者头像 李华
网站建设 2026/10/13 3:50:22

Django电商实战:从订单模型到并发扣库存的完整指南

简介&#xff1a;EcommerceWbsite 是基于 Python Django 框架的电子商务网站完整项目&#xff0c;面向 Django 入门与中级 Web 开发者&#xff0c;尤其适合需要课程设计或实战项目参考的人群。项目前端以 HTML 与 Django 模板语言&#xff08;DTL&#xff09;结合实现&#xff…

作者头像 李华
网站建设 2026/10/13 3:49:07

深入理解Linux文件描述符:系统调用、重定向与缓冲区机制

不管你是刚学 Linux 系统编程&#xff0c;还是写 C/C 写了好一阵子却总在printf和write之间犯迷糊&#xff0c;文件描述符&#xff08;fd&#xff09;都是一道绕不过去的坎。我们每天都在用ls > out.txt把命令结果存进文件&#xff0c;也见过2>&1把报错信息一起重定向…

作者头像 李华
网站建设 2026/10/13 3:48:40

基于SpringBoot+Vue的校园招聘网站实战:从架构设计到部署全解析

校园招聘网站这个题目&#xff0c;在毕业设计和课程项目里出现的频率真不低。但大多数网上的资料只是把代码堆上去&#xff0c;讲不清为什么这样设计&#xff0c;更别提调试阶段那些踩坑记录了。这次就以一个基于SpringBoot Vue的校园招聘网站为例&#xff0c;把项目从架构设计…

作者头像 李华
网站建设 2026/10/13 3:48:25

Gemini CLI 配置指南:settings.json、环境变量与命令行参数全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 3:47:52

计算机系统基础:从五大部件到存储程序,彻底搞懂机器底层

经常有初学的朋友跑来问我&#xff1a;计算机系统基础里“基本组成”这一章&#xff0c;是不是把输入设备、输出设备、存储器、运算器、控制器这张图背熟就完事了&#xff1f;如果只是为了应付期末考试&#xff0c;背图也许够了&#xff1b;但你要是真的想知道“一段代码写出来…

作者头像 李华