什么是服务端模板注入漏洞(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模板引擎](https://www.bilibili.com/video/BV1EV411h79m) \[2\] [JavaWeb-JSP原理深度解析](https://www.bilibili.com/video/BV1Z3411C7NZ?spm_id_from=333.788.videopod.episodes&vd_source=beeb967a84b38ad9b59e7206ed42a531&p=36) \[3\] 《TOMCAT与JAVA WEB 开发技术详解 第3版》 \[4\] [deepseek](https://chat.deepseek.com/) \[5\] [mimo](https://aistudio.xiaomimimo.com/#/chat)

相关推荐
网安薯条3 小时前
Kali Linux 虚拟机安装与基础配置保姆级图文教程
linux·运维·网络·安全·web安全·网络安全
C_lea4 小时前
公钥私钥密钥
计算机网络·网络安全
菩提小狗4 小时前
每日安全情报报告 · 2026-05-05
网络安全·漏洞·cve·安全情报·每日安全
X7x55 小时前
虚拟专用网络:企业网络安全的隐形守护者
网络安全·网络攻击模型·安全威胁分析·安全架构·vpn
m0_738120726 小时前
Webshell流量分析——常见扫描器AWVS,goby,xray流量特征分析
服务器·前端·安全·web安全·网络安全
Rytter18 小时前
某气骑士 libtprt.so 反 Frida 机制分析与绕过
android·安全·网络安全
天都35721 小时前
青少年ctf 日志排查 复盘
windows·网络安全·应急响应
X7x51 天前
数字时代的守护者:抗DDoS设备构筑业务永续的基石
网络安全·网络攻击模型·安全威胁分析·ddos·安全架构