Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Thymeleaf SSTI 分析以及最新版修复的 Bypass

Thymeleaf SSTI 分析以及最新版修复的 Bypass

作者头像
p4nda
发布于 2023-01-03 06:46:36
发布于 2023-01-03 06:46:36
2.4K00
代码可运行
举报
文章被收录于专栏:技术猫屋技术猫屋
运行总次数:0
代码可运行

0x01 写在前面

前段时间补上了迟迟没有写的 文件包含漏洞原理与实际案例介绍一文,在其中就提到了 Thymeleaf SSTI 漏洞,昨天在赛博群里三梦师傅扔了一个随手挖的 CVE——Thymeleaf SSTI Bypass,想着之前项目的代码还没清理,一起分析来看看

0x02 Thymeleaf SSTI

Thymeleaf 是与 java 配合使用的一款服务端模板引擎,也是 Spring 官方支持的一款服务端模板引擎。而 SSTI 最初是由 James Kettle 提出研究,Emilio Pinna 对他的研究进行了补充,不过这些作者都没有对 Thymeleaf 进行 SSTI 相关的漏洞研究工作,后来 Aleksei Tiurin 在 ACUNETIX 的官方博客上发表了关于 Thymeleaf SSTI 的文章,因此​ Thymeleaf SSTI 逐渐被安全研究者关注。

为了更方便读者理解这个 Bypass,因此在这里简单说一遍一些基础性的内容,如果了解的,可以直接跳到 0x03 的内容。

Thymeleaf 表达式可以有以下类型:

  • ${...}:变量表达式 —— 通常在实际应用,一般是OGNL表达式或者是 Spring EL,如果集成了Spring的话,可以在上下文变量(context variables )中执行
  • *{...}: 选择表达式 —— 类似于变量表达式,区别在于选择表达式是在当前选择的对象而不是整个上下文变量映射上执行。
  • #{...}: Message (i18n) 表达式 —— 允许从外部源(比如.properties文件)检索特定于语言环境的消息
  • @{...}: 链接 (URL) 表达式 —— 一般用在应用程序中设置正确的 URL/路径(URL重写)。
  • ~{...}:片段表达式 —— Thymeleaf 3.x 版本新增的内容,分段段表达式是一种表示标记片段并将其移动到模板周围的简单方法。 正是由于这些表达式,片段可以被复制,或者作为参数传递给其他模板等等

实际上,Thymeleaf 出现 SSTI 问题的主要原因也正是因为这个片段表达式,我们知道片段表达式语法如下:

  1. ~{templatename::selector},会在/WEB-INF/templates/目录下寻找名为templatename的模版中定义的fragment

如有一个 html 文件的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!DOCTYPE html> 
<html xmlns:th="http://www.thymeleaf.org"> 
<body> <div th:fragment="banquan"> &copy; 2021 ThreeDream yyds</div> 
</body> 
</html>

然后在另一template中可以通过片段表达式引用该片段:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<div th:insert="~{footer :: banquan}"></div>

th:insertth:replace:插入片段是比较常见的用法

  1. ~{templatename},引用整个templatename模版文件作为fragment

这个也比较好理解,不做详细举例

  1. ~{::selector} ~{this::selector},引用来自同一模版文件名为selectorfragmnt

在这里,selector可以是通过th:fragment定义的片段,也可以是类选择器、ID选择器等。

  1. 当**~{}**片段表达式中出现**::**,那么 ::**后需要有值(也就是**selector**)**

在了解这些内容后,我们就可以正式来看这个漏洞是怎么一回事了。

首先,同样的,我们拿一个常见的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@GetMapping("/admin") 
public String path(@RequestParam String language) 
{ 
    return "language/" + language + "/admin"; 
}

这是 SpringBoot 项目中某个控制器的部分代码片段,thymeleaf 的目录如下:

从代码逻辑中基本上可以判断,这实际上是一个语言界面选择的功能,如果是中文阅读习惯者,那么会令language参数为cn,如果是英文阅读习惯者,那么会令language参数为en,代码逻辑本身实际上是没有什么问题的,但是这里采用的是 thymeleaf 模板,就出现了问题。

在springboot + thymeleaf 中,如果视图名可控,就会导致漏洞的产生。其主要原因就是在控制器中执行 return 后,Spring 会自动调度 Thymeleaf 引擎寻找并渲染模板,在寻找的过程中,会将传入的参数当成SpEL表达式执行,从而导致了远程代码执行漏洞。

thymeleaf 渲染的流程如下:

  • createView() 根据视图名创建对应的View
  • renderFragment() 根据视图名解析模板名称

所以可以跟进renderFragment()来看看如何解析模板名称的:

核心代码我复制了出来:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected void renderFragment(Set<String> markupSelectorsToRender, Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {  
    String templateName;  
    Set<String> markupSelectors, processMarkupSelectors;  
    ServletContext servletContext = getServletContext();  
    String viewTemplateName = getTemplateName();  
    ISpringTemplateEngine viewTemplateEngine = getTemplateEngine();
        ...
            if (!viewTemplateName.contains("::")) {
                templateName = viewTemplateName;
                markupSelectors = null;
            } else {
                IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);

                FragmentExpression fragmentExpression;
                try {
                    fragmentExpression = (FragmentExpression)parser.parseExpression(context, "~{" + viewTemplateName + "}");
                } catch (TemplateProcessingException var25) {
                    throw new IllegalArgumentException("Invalid template name specification: '" + viewTemplateName + "'");
                }

        ...

可以发现,这里将模板名称(viewTemplateName) 进行拼接 "~{" + viewTemplateName + "}",然后使用parseExpression进行解析,继续跟进parseExpression就可以发现

会通过EngineEventUtils.computeAttributeExpression将属性计算成表达式:

然后再进行预处理(预处理是在正常表达式之前完成的执行,可以理解成预处理就解析并执了行表达式),最终执行了表达式。

效果如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
http://127.0.0.1:8080/admin?language=__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()}__::.k

这个 POC 为什么这样构造呢?

前文在介绍renderFragment函数的时候我们提到,renderFragment在解析模板名称的时候会将模板名称进行拼接 "~{" + viewTemplateName + "}",然后使用parseExpression进行解析,我们跟进parseExpression

进入org.thymeleaf.standard.expression StandardExpressionParser.java中的 parseExpression方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
(preprocess? StandardExpressionPreprocessor.preprocess(context, input) : input);

可以发现对表达式进行了preprocess预处理,跟进该方法:

preprocess预处理会解析出__xx__中间的部分作为表达式

如果 debug 可以发现,该表达式最终在org.thymeleaf.standard.expression.VariableExpression.executeVariableExpression()中作为 SpEL表达式执行。

因此 POC 中我们要构造形如__xx__的SpEL表达式(SpEL相关的知识点可以参考此文:SPEL 表达式注入漏洞深入分析),即表达式要为:__${xxxxx}__ 这种形式

那么为什么后面还有带有::呢?

因为renderFragment中的判断条件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (!viewTemplateName.contains("::")) {

即只有当模板名包含::时,才能够进入到parseExpression,也才会将其作为表达式去进行执行。

至于 POC 最后的.k,我们在最开始的提到了:

~{}片段表达式中出现::,那么 ::后需要有值(也就是selector

因此,最终 POC 的形式就为:__${xxxx}__::.x

实际上,只有3.x版本的Thymeleaf 才会受到影响,因为在2.x 中renderFragment的核心处理方法是这样的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected void renderFragment(Set<String> markupSelectorsToRender, Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

        ...

                Configuration configuration = viewTemplateEngine.getConfiguration();
                ProcessingContext processingContext = new ProcessingContext(context);
                templateCharacterEncoding = getStandardDialectPrefix(configuration);
                StandardFragment fragment = StandardFragmentProcessor.computeStandardFragmentSpec(configuration, processingContext, viewTemplateName, templateCharacterEncoding, "fragment");
                if (fragment == null) {
                    throw new IllegalArgumentException("Invalid template name specification: '" + viewTemplateName + "'");
                }
                
        ...

并没有3.x 版本中对于片段表达式(~{)的处理,也因此不会造成 SSTI 漏洞,以下是 SpringBoot 默认引用的 thymeleaf 版本

spring boot:1.5.1.RELEASE spring-boot-starter-thymeleaf:2.1.5 spring boot:2.0.0.RELEASE spring-boot-starter-thymeleaf:3.0.9 spring boot:2.2.0.RELEASE spring-boot-starter-thymeleaf:3.0.11

0x03 Thymeleaf SSTI Bypass

针对上文中的问题,Thymeleaf 实际上做了修复:

3.0.12 版本,Thymeleaf 在 util目录下增加了一个名为SpringStandardExpressionUtils.java的文件:

在该文件中,就有说明:

当调用表达式的时候,会经过该函数的判断:

来看看该函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static boolean containsSpELInstantiationOrStatic(final String expression) {

        final int explen = expression.length();
        int n = explen;
        int ni = 0; // index for computing position in the NEW_ARRAY
        int si = -1;
        char c;
        while (n-- != 0) {

            c = expression.charAt(n);

            if (ni < NEW_LEN
                    && c == NEW_ARRAY[ni]
                    && (ni > 0 || ((n + 1 < explen) && Character.isWhitespace(expression.charAt(n + 1))))) {
                ni++;
                if (ni == NEW_LEN && (n == 0 || !Character.isJavaIdentifierPart(expression.charAt(n - 1)))) {
                    return true; // we found an object instantiation
                }
                continue;
            }

            if (ni > 0) {
                n += ni;
                ni = 0;
                if (si < n) {
                    // This has to be restarted too
                    si = -1;
                }
                continue;
            }

            ni = 0;

            if (c == ')') {
                si = n;
            } else if (si > n && c == '('
                        && ((n - 1 >= 0) && (expression.charAt(n - 1) == 'T'))
                        && ((n - 1 == 0) || !Character.isJavaIdentifierPart(expression.charAt(n - 2)))) {
                return true;
            } else if (si > n && !(Character.isJavaIdentifierPart(c) || c == '.')) {
                si = -1;
            }

        }
        return false;
    }

可以看到其主要逻辑是首先 倒序检测是否包含 wen关键字、在(的左边的字符是否是T,如包含,那么认为找到了一个实例化对象,返回true,阻止该表达式的执行。

因此要绕过这个函数,只要满足三点:

1、表达式中不能含有关键字new

2、在(的左边的字符不能是T

3、不能在T(中间添加的字符使得原表达式出现问题

三梦师傅给出的答案是%20(空格),在我研究中发现其实还有%0a(换行)、%09(制表符),此外,通过 fuzzing 同样可以找到很多可以利用的字符:

有兴趣的朋友可以自己测试还有哪些可以绕过

需要注意的是,这种绕过方式针对的情景是当传入的路径名可控时,如:

这里有一个点需要注意,可以看到上面一个图片中 path 和返回的视图名不一样,path 为/admin/*,返回的视图名为language/cn/*,但当 path 和返回的视图名一样的时候,如下:

实际上上述payload 是没有用的

为什么呢?

实际上在 3.0.12 版本,除了加了SpringStandardExpressionUtils.java,同样还增加了 SpringRequestUtils.java文件:

并且看其描述:

如果视图名称包含在 URL 的路径或参数中,请避免将视图名称作为片段表达式执行

意思就是如果视图的名字和 path 一致,那么就会经过SpringRequestUtils.java中的checkViewNameNotInRequest函数检测:

可以看到,如果requestURI不为空,并且不包含vn的值,即可进入判断,从而经过checkViewNameNotInRequest的“良民”认证。

首先按照上文中的 Poc:__${T%20(java.lang.Runtime).getRuntime().exec(%22open%20-a%20calculator%22)}__::.x/

我们可以得到 vn 的值为home/__${t(java.lang.runtime).getruntime().exec("open-acalculator")}__::.x

既然vn的值确定下来,那么接下来只要令requestURI.contains(vn)为假,就能达到我们的目的

contains 区分大小写,那么……

别想了,因为 pack 方法已经经过了toLowerCase处理

那么是不是么办法了?答案是否定的(废话,三梦师傅给出了答案)

我们先看requestURI是怎么来的:

跟进unescapeUriPath方法:

跟进unescapeUriPath方法:

调用了UriEscapeUtil.unescape,跟进:

该函数首先检测传入的字符中是否是%(ESCAPE_PREFIX)或者+,如果是,那么进行二次处理:

  • +转义成空格
  • 如果%的数量大于一,需要一次将它们全部转义

处理完毕后,将处理后的字符串返还回

如果实际不需要unescape,那么不经过处理,直接返回原始字符串对象

最终,就得到了requestURI

貌似,也没啥特殊的地方

既然没有特殊的地方,那么我们只需要思考,如何从正面令requestURI.contains(vn)为假,即令requestURI不等于home/__${t(java.lang.runtime).getruntime().exec("open-acalculator")}__::.x即可

这件事本质是令两个字符串不相等,并且要满足路由条件(/home/*路径下)

那么结论就来了

Bypass 技巧 1:

这也是三梦师傅在群里提到的

home;/__${t(java.lang.runtime).getruntime().exec("open-acalculator")}__::.x

只需要在 home 的后面加上一个分号即可

这是因为在 SpringBoot 中,SpringBoot 有一个功能叫做矩阵变量,默认是禁用状态:

如果发现路径中存在分号,那么会调用removeSemicolonContent方法来移除分号

这样一来使得传入的字符和vn不相同,并且又满足路由条件!成功绕过checkViewNameNotInRequest的检测

Bypass 技巧 2:

这个 Bypass 是我分析的时候想到的,前面也提到了,我们的实际目标就是令两个字符串不相等,并且要满足路由条件(/home/*路径下),那么:

home//__{t(java.lang.runtime).getruntime().exec("open-acalculator")}__::.x和home/__

最后提一点,实际上 payload 中不能含有/,否则会执行不成功:

原因其实就是路由条件不相等,因为解析器看来是这样的路径

/home;/__${T (java.lang.Runtime).getRuntime().exec("open -a /System /Applications /Calculator.app")}__::.x/

0x04 总结

遗憾的是,这 Bypass Thymeleaf 官方并没有给三梦师傅分配 CVE,和三梦师傅讨论认为,Thymeleaf 认为这是开发者需要注意到的地方(因为 return 的内容是由开发者控制,开发者应当注意这个问题),不过这个理由牵不牵强,就只能自己领会了

实际上由于时间问题,还有一些内容没有横向扩展,比如,当不 return 的时候:

能否 Bypass?

当模板内容可控的时候:

又能否 Bypass?

此外,java 常用的其他模板引擎,如 Velocity、Freemarker、Pebble 和 Jinjava 是否存在类似问题?

这些问题在我有时间的时候会尝试去解决,也同时欢迎其他师傅共同分析思考这些问题

项目源码我也已经上传到 GitHub 上了,有兴趣可以自己搭建看看,虽然很简单,但是可以省去复制代码的时间了

https://github.com/cn-panda/ThymeleafSSTIBypass

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-11-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
JAVA安全之Thymeleaf模板注入防护绕过
若依CMS中使用到了Thymeleaf模板引擎且存在模板注入可控点,但是在漏洞测试过程中发现常规的通用载荷并不生效,遂对其进行调试分析,最后发现是和Thymeleaf版本有莫大的关系,其中3.0.12版本增加了多处安全机制来防护模板注入漏洞,本篇文章将基于此背景对Thymeleaf模板的注入防御措施和绕过进行深入刨析
Al1ex
2024/10/14
2200
JAVA安全之Thymeleaf模板注入防护绕过
Thymeleaf 5 分钟教程
Thymeleaf 是一个用于 web 和独立环境的现代服务器端 Java 模板引擎。
acc8226
2022/05/17
4340
重学SpringBoot3-集成Thymeleaf
Thymeleaf 是一个现代的服务器端Java模板引擎,用于Web和独立环境。它能够处理HTML、XML、JavaScript、CSS甚至纯文本。Thymeleaf 的主要目标是提供一个优雅和高度可维护的创建模板的方式。为了实现这一点,它建立在自然模板的概念上,这意味着你可以将静态原型直接转换成动态模板,无需更改标记。 凭借 Spring Framework 的模块、与你喜爱的工具的大量集成以及插入你自己的功能的能力,Thymeleaf 非常适合现代 HTML5 JVM Web 开发。
CoderJia
2024/10/18
3450
重学SpringBoot3-集成Thymeleaf
Java代码审计| Spring框架知识篇
在上期的Java代码审计Spring框架思路篇中,斗哥为大家讲述了如何得到Spring审计的Demo,审计源码,根据Spring框架审计思路初步判定是否存在漏洞,剩下就是构造POC,动态调试分析修改POC。本期Java代码审计Spring框架知识篇将讲述Spring构造POC要必备的知识。
漏斗社区
2018/07/26
9870
Java代码审计| Spring框架知识篇
JAVA安全之Thymeleaf模板注入检测再探
从之前的文章中我们分析后发现Thymeleaf 3.0.15版本中只要检测到"{"就会认为存在表达式内容,随后直接抛出异常停止解析来防范模板注入问题,此类场景用于我们URL PATH、Retruen、Fragment等可控的情况下进行,但是如果我们存在对模板文件进行更改、创建、上传等操作的时候我们还可以精心构造恶意的JAVA代码并将其写入模板中,随后触发执行
Al1ex
2024/10/18
1200
JAVA安全之Thymeleaf模板注入检测再探
thymeleaf模板注入学习与研究--查找与防御
实际开发过程中 依靠我丰富的想象力只能想出 换主题 这种场景下可能会出现 大佬们自行脑补吧。
FB客服
2022/11/14
6340
thymeleaf模板注入学习与研究--查找与防御
【Java 代码审计入门-06】文件包含漏洞原理与实际案例介绍
为什么会有这一些列的文章呢?因为我发现网上没有成系列的文章或者教程,基本上是 Java 代码审计中某个点来阐述的,对于新人来说可能不是那么友好,加上本人也在学习 Java 审计,想做个学习历程的记录和总结,因此有了本系列的文章。
p4nda
2023/01/03
1.6K0
【Java 代码审计入门-06】文件包含漏洞原理与实际案例介绍
Thymeleaf的使用前言:一、thymeleaf简介:二、thymeleaf标准方言:三、thymeleaf与springboot集成案例:总结:
最近听说thymeleaf好像也挺流行的,还说是spring官方推荐使用,那thymeleaf究竟是什么呢?spring为什么推荐用它呢?怎么用呢?本文将为你揭秘!
贪挽懒月
2018/08/02
1K0
Thymeleaf的使用前言:一、thymeleaf简介:二、thymeleaf标准方言:三、thymeleaf与springboot集成案例:总结:
springboot 整合 thymeleaf(上手即用)
springboot 整合thymeleaf 其实用的不是很多,因为现在很多公司都是前后端分离的项目,通过接口交互了。但是我们后端人员,对前端不是很了解,但是又想做些东西看看效果。所以就可以整合 thymeleaf ,掌握一些基本的语法,就可以很好的操作啦。
用户5546570
2020/07/08
6470
springboot 整合 thymeleaf(上手即用)
Spring Web MVC框架(十二) 使用Thymeleaf
前面的例子我们使用的视图技术主要是JSP。JSP的优点是它是Java EE容器的一部分,几乎所有Java EE服务器都支持JSP。缺点就是它在视图表现方面的功能很少,假如我们想迭代一个数组之类的,只能使用<% %>来包括Java语句进行。虽然有标准标签库(JSTL)的补足,但是使用仍然不太方便。另外JSP只能在Java EE容器中使用,如果我们希望渲染电子邮件之类的,JSP就无能为力了。
乐百川
2022/05/05
2.9K0
java spel_SPEL表达式注入-入门篇
Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了额外的功能,特别是方法调用和基本的字符串模板功能。同时因为SpEL是以API接口的形式创建的,所以允许将其集成到其他应用程序和框架中。
全栈程序员站长
2022/10/01
2.5K0
java spel_SPEL表达式注入-入门篇
Java代码审计之SpEL表达式注入
Spring Expression Language(简称 SpEL)是一种功能强大的表达式语言、用于在运行时查询和操作对象图;语法上类似于 Unified EL,但提供了更多的特性,特别是方法调用和基本字符串模板函数。SpEL 的诞生是为了给 Spring 社区提供一种能够与 Spring 生态系统所有产品无缝对接,能提供一站式支持的表达式语言。
FB客服
2019/05/09
2K0
Java代码审计之SpEL表达式注入
spel表达式注入[通俗易懂]
大家好,又见面了,我是你们的朋友全栈君。 pom.xml中添加 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>5.0.8.RELEASE</version> </dependency> 测试 package com.example.demo.SpELTest; import org.springframework.exp
全栈程序员站长
2022/10/01
7700
spel表达式注入[通俗易懂]
Thymeleaf入门
在HTML页面上,加载Java内存中的数据的过程我们称为渲染(render);thymeleaf是用来帮助视图渲染的技术。
用户9184480
2024/12/13
610
Thymeleaf模板常用知识点thymeleaf介绍标准表达式语法常用th标签设置属性值Thymeleaf迭代循环
thymeleaf 是新一代的模板引擎,在spring4.0中推荐使用thymeleaf来做前端模版引擎。 thymeleaf介绍 简单说, Thymeleaf 是一个跟 Velocity、FreeMarker 类似的Java模板引擎,它可以完全替代 JSP 。相较与其他的模板引擎,它有如下三个极吸引人的特点: 1.Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它遵从web标准,支持 HTML 原型,然
JavaEdge
2018/05/16
3K0
SpringBoot入门系列(五)Thymeleaf的常用标签和用法
前面介绍了Spring Boot 中的整合Thymeleaf 。今天我们主要来看看 Thymeleaf 的常用标签和用法!其他详细的内容,大家可以看看Thymeleaf官方使用手册 。
架构师精进
2020/03/19
1.1K0
thymeleaf关于js的一些坑(数组定义)
乍一看,没问题,也许对应用过thymeleaf的小伙伴来说一眼就看出了问题,但是对于新手确实很难发现,thymeleaf会把[[]]中的内容作为内联取值块解析,而不是数组。
小尘哥
2018/08/15
1.7K0
Thymeleaf【快速入门】Thymeleaf介绍
然后官网还给出了一段看起来仍然像HTML一样工作的集成了Thymeleaf模版的代码,我们大致的来感受一下:
我没有三颗心脏
2019/01/03
3.8K0
SpringBoot 之 Web 使用 Thymeleaf 模板
SpringBoot 推荐使用 Thymeleaf,且默认不支持 JSP,因为 JSP 必须要打包war包才行。
AI码真香
2022/09/13
1.2K0
SpringBoot  之 Web 使用 Thymeleaf 模板
SpringBoot ( 四 ) :thymeleaf 使用详解
简单说, Thymeleaf 是一个跟 Velocity、FreeMarker 类似的模板引擎,它可以完全替代 JSP 。相较与其他的模板引擎,它有如下三个极吸引人的特点:
前朝楚水
2018/07/26
1.4K0
SpringBoot ( 四 ) :thymeleaf 使用详解
推荐阅读
相关推荐
JAVA安全之Thymeleaf模板注入防护绕过
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验