什么是服务端模板注入漏洞(SSTI)

文章目录

  • [1. SSTI概述](#1. SSTI概述)
  • [2. 动态网页](#2. 动态网页)
  • [3. 模板引擎](#3. 模板引擎)
  • [4. 服务端模板注入漏洞](#4. 服务端模板注入漏洞)
    • [4.1 模板拼接](#4.1 模板拼接)
    • [4.2 表达式注入](#4.2 表达式注入)
  • [5. 典型漏洞代码模式](#5. 典型漏洞代码模式)
  • 参考

1. SSTI概述

SSTI(Server-Side Template Injection,服务器端模板注入) 是一种利用Web应用程序中模板引擎的安全漏洞,通过注入恶意代码来执行服务器端命令的攻击方式。

好,如果没有学过Web开发,不知道什么是MVC模型,上面这段话理解起来会很抽象。但有一点我们是可以明确的,只要是Web漏洞,都是前端/客户端提交请求数据包,通过HTTP协议发送,后端/服务端接收请求包,处理完后生成响应数据包返回。既然叫"服务端模板注入",顾名思义就是前端提交的数据注入到了后端的模板中,可后端不是一些PHP、Java、Python等语言程序文件吗?

要回答什么是模板,前端数据怎么注入到模板,注入模板怎么产生漏洞,那就需要看看后端是怎么动态生成HTML文件的,下面,我们就要尝试在有限的篇幅中描述清楚这一过程及其中的一些概念。

2. 动态网页

一个网页就是一个HTML文件,浏览器负责解析这个文件并展示。动态网页到底动态在哪?我们所有用户都可以登录CSDN,登录成功后返回给用户的HTML页面几乎是一致的,只是用户名、头像等略有差别。服务端不可能为每一个用户事先准备一个静态的HTML,分别为每一个用户返回,这样服务器存储会爆炸。

很明显,我们在CSDN登录成功后返回的HTML文件的骨架是固定的,只是在头像等区域换成各自用户的数据。后端就是根据这个固定的HTML骨架,并在相应位置插入用户的数据,形成最终返回给各自用户的HTML文件。

说起来很简单,要实现这个过程很繁琐。HTML文件本质就是一行行的字符串/文本,古早时候后端程序也确实是通过字符串拼接来动态生成HTML文件,这是一个怎样的过程呢?我们以Java为例子展示一下查询数据库获取数据后并生成HTML文件的过程,

java 复制代码
public class DeptListServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        // 获取应用的根路径
        String contextPath = request.getContextPath();

        // 设置响应的内容类型以及字符集。防止中文乱码问题。
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();

        out.print("<!DOCTYPE html>");
        out.print("<html>");
        out.print("	<head>");
        out.print("		<meta charset='utf-8'>");
        out.print("		<title>部门列表页面</title>");

        out.print("<script type='text/javascript'>");
        out.print("    function del(dno){");
        out.print("        if(window.confirm('亲,删了不可恢复哦!')){");
        out.print("            document.location.href = '"+contextPath+"/dept/delete?deptno=' + dno");
        out.print("        }");
        out.print("    }");
        out.print("</script>");

        out.print("	</head>");
        out.print("	<body>");
        out.print("		<h1 align='center'>部门列表</h1>");
        out.print("		<hr >");
        out.print("		<table border='1px' align='center' width='50%'>");
        out.print("			<tr>");
        out.print("				<th>序号</th>");
        out.print("				<th>部门编号</th>");
        out.print("				<th>部门名称</th>");
        out.print("				<th>操作</th>");
        out.print("			</tr>");
        /*========================上面一部分是固定不变的========================*/

        // 连接数据库,查询所有的部门
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // 获取连接
            conn = DBUtil.getConnection();
            // 获取预编译的数据库操作对象
            String sql = "select deptno as a,dname,loc from dept";
            ps = conn.prepareStatement(sql);
            // 执行SQL语句
            rs = ps.executeQuery();
            // 处理结果集
            int i = 0;
            while(rs.next()){
                String deptno = rs.getString("a");
                String dname = rs.getString("dname");
                String loc = rs.getString("loc");

                out.print("			<tr>");
                out.print("				<td>"+(++i)+"</td>");
                out.print("				<td>"+deptno+"</td>");
                out.print("				<td>"+dname+"</td>");
                out.print("				<td>");
                out.print("					<a href='javascript:void(0)' onclick='del("+deptno+")'>删除</a>");
                out.print("					<a href='"+contextPath+"/dept/edit?deptno="+deptno+"'>修改</a>");
                out.print("					<a href='"+contextPath+"/dept/detail?fdsafdsas="+deptno+"'>详情</a>");
                out.print("				</td>");
                out.print("			</tr>");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            DBUtil.close(conn, ps, rs);
        }

        /*=====================下面一部分是固定不变的=========================*/
        out.print("		</table>");
        out.print("		<hr >");
        out.print("		<a href='"+contextPath+"/add.html'>新增部门</a>");
        out.print("	</body>");
        out.print("</html>");
    }
}

在上面这个查看部门列表的代码里,部门的数量关乎列表的行数,部门数量是通过数据库动态查询出来的,也就是说HTML文件里的表格里地数据和表格行数需要动态地生成。那么我们代码里就需要取数据+拼接HTML格式地标签字符串,程序的逻辑和视图混合在一起,不利于前后端的分工,也使得程序的开发很繁琐。

自然而然,人们的解决办法就是:提供一个视图模板文件(HTML文件),里面有关于数据渲染的占位符,提供一个包含占位符信息的数据对象,有一个软件库,可以把数据直接渲染进模板,这个软件就是模板引擎。

3. 模板引擎

我们来实现一个功能极其单一分模板引擎,帮助我理解"什么是模板"、"什么是数据"、"什么是模板引擎"。

javascript 复制代码
<script>
var templateStr = '<h1>我在用{{lang}},实现一个简易的{{engine}}';
var data = {lang:'js', engine:'模板引擎'};
function render(templateStr, data){
	return templateStr.replace(/\{\{(\w+)\}\}/g, function(findStr, $1){
		return data[$1];
	});
}
var result = render(templateStr, data);
console.log(result)
</script>

在浏览器控制台中运行,发现成功"将数据渲染到了模板"。

4. 服务端模板注入漏洞

4.1 模板拼接

传统的MVC框架开发,生成HTML文件这一步是在后端完成的。现代前后端分离结构,可以简单理解为将MVC中的"C(控制)"和"V(视图)"放到了前端。

后端的模板引擎提供的功能是将数据填充进模板占位符,模板中的占位符并不单单是一个静态标记,而是一套有严格形式的指令。这些模板指令定义了该从哪里取数据,如何渲染数据等。模板引擎解析模板时,遇到对应的指令会执行对应的操作。服务器端模板注入(SSTI)的根源不在于模板引擎本身,而在于错误的代码编写模式。

模板语法 vs 模板内容

复制代码
安全的场景(数据层):
─────────────────────
模板:  "Hello, ${name}"
数据:  name = "<script>alert(1)</script>"
结果:  Hello, <script>alert(1)</script>
        ↑ name 的值被当作"数据"输出,不会被二次解析为模板语法


危险的场景(代码层):
─────────────────────
模板字符串:  "Hello, " + userInput   ← userInput 被拼进了模板本身
userInput = "${7*7}"
模板引擎解析的字符串:  "Hello, ${7*7}"
结果:  Hello, 49
        ↑ 用户输入变成了"模板代码",被执行了

漏洞模式:将用户输入直接拼接进模板

4.2 表达式注入

SSTI广义上包括任何模板指令注入,因为模板引擎提供的动态能力主要是通过表达式语言实现,即表达式注入是其最常见形式,攻击者通过注入表达式来突破数据边界执行代码。JSP的EL注入就是向JSP模板注入EL表达式,Spring的SpEL注入在模板场景下也是向模板注入SpEL表达式。因此,对于这类漏洞,核心都是表达式注入,表达式的执行权限决定了漏洞的危害程度。

5. 典型漏洞代码模式

CVE-2026-40478:Thymeleaf 模板注入

参考

1 Vue源码解析之mustache模板引擎

2 JavaWeb-JSP原理深度解析

3 《TOMCAT与JAVA WEB 开发技术详解 第3版》

4 deepseek

5 mimo

相关推荐
huaqisafty1 天前
网络赌博治安治理难点解析
网络安全·华企盾·社会治安治理·网赌赌博治理
顾城猿1 天前
zk‑SNARK 三者结合的完整工作原理
网络安全
数据知道1 天前
国密算法实战——SM2/SM3/SM4 在国产系统中的应用
网络·算法·安全·网络安全·密码学·哈希算法
Eason_LYC1 天前
你天天用的AI工具,藏着无需登录的高危后门 CVE-2025-3248
网络安全·渗透测试·漏洞复现·白帽子·langflow·远程代码执行·cve-2025-3248
隐擎fox1 天前
跨越传输层防线:深入 TCP/IP 协议栈指纹(p0f)原理与 Python 原始套接字检测实战
python·网络协议·tcp/ip·网络安全·dns
学逆向的1 天前
inilne HOOK
前端·网络安全·hook
持敬chijing1 天前
CentOS 从零搭建 LAMP 环境完整实战指南
linux·运维·web安全·网络安全·centos
Chengbei111 天前
红队新工具 ToShell 解析:自带 Web 控制台,AES+SM4 国密加密,载荷随机免杀,自主 Agent 自动完成渗透任务
人工智能·tcp/ip·安全·web安全·http·网络安全·系统安全
数据知道1 天前
量子计算威胁——后量子密码(PQC)迁移实战
安全·网络安全·密码学·量子计算
隐擎fox2 天前
深入理解网络传输层安全:TLS 指纹识别(JA3/JA4)原理与 Python 协议层检测实战
爬虫·python·网络协议·安全·网络安全·https