关于 sast-ssti
用于代码库定向安全评估的 SSTI 检测 skill。它发现动态模板渲染点、分批追踪用户输入并合并结果;运行前需要 `sast/architecture.md`。
主要功能
- 01发现动态模板渲染点
- 02批量追踪用户输入
- 03检查静态模板与白名单
- 04合并分析结果报告
适用场景
- 01审查用户提供模板的预览功能
- 02检查动态模板名称的使用
- 03追踪模板渲染路径中的输入流
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ utkusen/ sast-skills --skill sast-ssti安装后在 Claude Code 中输入/sast-ssti确认已加载。
详细介绍
服务端模板注入(SSTI)检测
你正在进行有针对性的安全评估,以查找代码库中的服务端模板注入漏洞。此技能采用由子 Agent 执行的三阶段方法:侦察(查找模板字符串为动态内容的候选渲染位置)、分批验证(追踪用户输入是否到达每个位置的模板参数,每批并行处理 3 个位置)以及合并(汇总各批次结果,生成最终报告)。
前置条件:必须存在 sast/architecture.md。如果不存在,请先运行分析技能。
什么是 SSTI
服务端模板注入发生在用户提供的输入被直接嵌入模板字符串,并由模板引擎进行求值时。与将用户数据作为上下文变量传递给静态模板不同,SSTI 意味着用户可以编写模板语法,由引擎执行,进而导致任意代码执行、读取文件或服务器被完全攻陷。
其核心模式:未经验证的用户输入被用作传递给模板引擎的 render/compile/evaluate 函数的模板字符串。
什么情况属于 SSTI
-
将用户输入作为待编译或渲染的模板字符串:
Template(user_input).render()— Jinja2env.from_string(user_input).render()— Jinja2render_template_string(user_input)— Flaskejs.render(user_input, ctx)— EJS(Node.js)nunjucks.renderString(user_input, ctx)— NunjucksHandlebars.compile(user_input)(ctx)— Handlebarspug.render(user_input, ctx)— Pug/Jade_.template(user_input)(ctx)— Lodash/UnderscoreVelocity.evaluate(ctx, user_input)— Apache Velocity(Java)new Template("anon", new StringReader(user_input), cfg).process(...)— FreeMarker(Java)new ST(user_input).render()— StringTemplate4(Java)thymeleafEngine.process(user_input, ctx)— Thymeleaf(Java)\Twig\Environment::createTemplate(user_input)->render(ctx)— Twig(PHP)$smarty->fetch("string:" . user_input)— Smarty(PHP)Liquid::Template.parse(user_input).render(ctx)— Liquid(Ruby)ERB.new(user_input).result(binding)— ERB(Ruby)t, _ := template.New("x").Parse(user_input); t.Execute(w, data)— Gotext/templateTemplate.fromString(user_input).render(ctx)— Pebble(Java)
-
动态构造模板名称,且名称本身来自用户输入,引擎会据此解析任意文件:
render_template(user_input)(Flask),其中user_input未通过安全列表验证res.render(req.query.template)(Express),其中模板名称由用户控制
什么情况不属于 SSTI
不要标记以下模式:
- 将用户输入作为上下文数据(安全——模板是静态的,只有数据会变化):
render_template("profile.html", name=request.args.get("name")) env.get_template("report.html").render(user=user_obj) res.render("dashboard", { title: req.body.title }) - 通过模板输出导致的 XSS:如果模板输出了未经清理的用户数据,并在浏览器中渲染,那是 XSS,而非 SSTI
- 模板文件名动态变化,但已通过允许列表验证:如果模板名称来自用户输入,但严格限制在硬编码的允许模板名称集合内,则不属于 SSTI
- 配置了受限环境的沙盒模板引擎:即使模板字符串来自用户输入,Liquid、Mustache 及类似的无逻辑模板引擎也无法执行任意代码——但除非能确认引擎为无逻辑引擎,否则仍应标记为“需要人工审核”
可防止 SSTI 的模式
遇到以下模式时,代码很可能不存在漏洞:
1. 静态模板文件搭配动态上下文(最常见的安全模式)
# Flask — static template, user input only in context dict
return render_template("user_profile.html", username=request.args.get("name"))
# Express — static view name
res.render("dashboard", { user: req.user })
2. 对模板名称进行允许列表验证
ALLOWED_TEMPLATES = {"invoice.html", "receipt.html", "summary.html"}
template_name = request.args.get("tmpl", "invoice.html")
if template_name not in ALLOWED_TEMPLATES:
abort(400)
return render_template(template_name)
3. 不支持代码执行的无逻辑/沙盒引擎
// Mustache — logic-less, cannot execute arbitrary code even if template is user-supplied
const output = Mustache.render(userTemplate, ctx); // lower risk, but still flag for review
易受攻击与安全示例
Python — Flask / Jinja2
# VULNERABLE: user input rendered as template string
@app.route('/greet')
def greet():
name = request.args.get('name', '')
template = f"<h1>Hello {name}!</h1>"
return render_template_string(template)
# Payload: ?name={{7*7}} → renders "49"
# RCE: ?name={{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# SECURE: user input passed as context variable to a static template
@app.route('/greet')
def greet():
name = request.args.get('name', '')
return render_template("greet.html", name=name)
# VULNERABLE: env.from_string with user-controlled template
@app.route('/preview')
def preview():
tmpl = request.form.get('template')
return Environment().from_string(tmpl).render()
# SECURE: load template from trusted file, pass user data as context
@app.route('/preview')
def preview():
data = request.form.get('data')
return env.get_template("preview.html").render(data=data)
Node.js — EJS
// VULNERABLE: user input as template string
app.get('/render', (req, res) => {
const tmpl = req.query.template;
res.send(ejs.render(tmpl, { user: req.user }));
// Payload: ?template=<%- global.process.mainModule.require('child_process').execSync('id') %>
});
// SECURE: user input only in context data
app.get('/render', (req, res) => {
res.render('report', { content: req.query.content });
});
Node.js — Nunjucks
// VULNERABLE: renderString with user-controlled template
app.post('/preview', (req, res) => {
const output = nunjucks.renderString(req.body.tmpl, { user: req.user });
res.send(output);
// Payload: {{ range.constructor("return global.process.mainModule.require('child_process').execSync('id').toString()")() }}
});
// SECURE: render from a file, user input only as context
app.post('/preview', (req, res) => {
res.render('preview.html', { content: req.body.content });
});
Node.js — Handlebars
// VULNERABLE: compile with user-supplied template string
app.get('/email', (req, res) => {
const template = Handlebars.compile(req.query.tmpl);
res.send(template({ user: req.user }));
// Payload: {{#with "s" as |string|}}{{#with "e"}}{{#with split as |conslist|}}...
});
// SECURE: compile static template, user data in context
const template = Handlebars.compile(fs.readFileSync('email.hbs', 'utf8'));
app.get('/email', (req, res) => {
res.send(template({ name: req.query.name }));
});
Ruby — ERB
# VULNERABLE: user input passed to ERB constructor
get '/render' do
tmpl = params[:template]
ERB.new(tmpl).result(binding)
# Payload: <%= `id` %>
end
# SECURE: static ERB file, user data in binding only
get '/render' do
@name = params[:name]
erb :profile
end
Java — FreeMarker
// VULNERABLE: template string sourced from user input
@PostMapping("/preview")
public String preview(@RequestParam String tmplStr, Model model) throws Exception {
Template t = new Template("preview", new StringReader(tmplStr), cfg);
StringWriter out = new StringWriter();
t.process(model.asMap(), out);
return out.toString();
// Payload: <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
}
// SECURE: load template from classpath, user data only in model
@GetMapping("/report")
public String report(@RequestParam String userId, Model model) {
model.addAttribute("user", userService.findById(userId));
return "report"; // resolves to templates/report.ftl
}
Java — Velocity
// VULNERABLE: user input evaluated as template
public String render(String userTemplate) {
VelocityContext ctx = new VelocityContext();
StringWriter sw = new StringWriter();
Velocity.evaluate(ctx, sw, "template", userTemplate);
return sw.toString();
// Payload: #set($e="")#set($x=$e.class.forName("java.lang.Runtime"))...
}
// SECURE: load template from file
Template t = Velocity.getTemplate("report.vm");
t.merge(ctx, sw);
Java — Thymeleaf (Spring)
// VULNERABLE: user input used as template expression evaluated by Thymeleaf
@GetMapping("/hello")
public String hello(@RequestParam String lang, Model model) {
return "user/" + lang + "/welcome"; // path traversal + SSTI if lang is e.g. "__${T(java.lang.Runtime).getRuntime().exec('id')}"
}
// SECURE: validate lang against an allowlist
private static final Set<String> ALLOWED_LANGS = Set.of("en", "fr", "de");
@GetMapping("/hello")
public String hello(@RequestParam String lang, Model model) {
if (!ALLOWED_LANGS.contains(lang)) return "error";
return "user/" + lang + "/welcome";
}
PHP — Twig
// VULNERABLE: user input as template string
$app->get('/render', function (Request $request) use ($twig) {
$tmpl = $request->query->get('template');
return $twig->createTemplate($tmpl)->render([]);
// Payload: {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
});
// SECURE: static template, user data in context array
$app->get('/profile', function (Request $request) use ($twig) {
return $twig->render('profile.html.twig', ['name' => $request->query->get('name')]);
});
PHP — Smarty
// VULNERABLE: user-controlled template string via fetch("string:...")
$template = $_GET['tmpl'];
$smarty->fetch("string:" . $template);
// Payload: {php}echo shell_exec('id');{/php}
// SECURE: pass user data as template variable
$smarty->assign('name', $_GET['name']);
$smarty->display('profile.tpl');
Go — text/template
// VULNERABLE: user input parsed as template
func handler(w http.ResponseWriter, r *http.Request) {
tmpl := r.URL.Query().Get("tmpl")
t, _ := template.New("x").Parse(tmpl)
t.Execute(w, data)
// Payload: {{.Func "os/exec" "id"}} — depends on data methods exposed
}
// SECURE: static template string or file; user input only in data
func handler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("tmpl/page.html"))
t.Execute(w, map[string]string{"Name": r.URL.Query().Get("name")})
}
// Note: Go's html/template auto-escapes output, but text/template does not.
// Even html/template is vulnerable to SSTI if user input reaches .Parse().
执行
此技能分三个阶段运行,使用子 Agent。将 sast/architecture.md 的内容作为上下文传递给所有子 Agent。
阶段 1:查找使用动态字符串的模板渲染位置
启动一个子 Agent,并提供以下指令:
目标:找出代码库中所有将动态构建的字符串本身作为模板,而非加载静态模板文件的模板引擎渲染、编译或求值位置。将结果写入
sast/ssti-recon.md。背景:你会获得项目架构摘要。请据此了解技术栈、使用的模板引擎,以及视图和响应的渲染方式。
需要搜索的内容 — 易受攻击的模板渲染模式:
标记所有第一个参数(模板字符串)为变量、拼接字符串或任何非字面值的调用。此阶段暂不检查该变量是否来自用户输入;那是第 2 阶段的工作。
- Python — Jinja2 / Flask:
render_template_string(var)— 参数为任何非字面值的情况Environment().from_string(var)或env.from_string(var)jinja2.Template(var).render(...)Template(var),其中 Template 是从 jinja2 导入的- Python — Mako:
Template(var).render(...),其中 Template 来自mako.templatemako.template.Template(var)- Node.js — EJS:
ejs.render(var, ...)或ejs.renderFile(var, ...),其中 var 不是静态字符串字面值- Node.js — Nunjucks:
nunjucks.renderString(var, ...)— 第一个参数为任何非字面值的情况env.renderString(var, ...)- Node.js — Handlebars:
Handlebars.compile(var)— 参数为任何非字面值的情况Handlebars.precompile(var)- Node.js — Pug/Jade:
pug.render(var, ...)— 参数为任何非字面值的情况pug.compile(var, ...)- Node.js — Lodash/Underscore:
_.template(var)— 参数为任何非字面值的情况Handlebars.compile(var)- Node.js — Swig / Twig.js:
swig.render(var, ...)twig({ data: var })- Ruby — ERB:
ERB.new(var).result(...)— 参数为任何非字面值的情况ERB.new(var).result_with_hash(...)- Ruby — Liquid:
Liquid::Template.parse(var).render(...)— 参数为任何非字面值的情况- Java — FreeMarker:
new Template(name, new StringReader(var), cfg)— var 不是字面值cfg.getTemplate(var),其中 var 不是字面值(可能存在模板路径注入)- Java — Velocity:
Velocity.evaluate(ctx, writer, logTag, var)— 第四个参数为任何非字面值的情况ve.evaluate(ctx, writer, logTag, var)- Java — StringTemplate / ST4:
new ST(var)— 参数为任何非字面值的情况new STGroup(var, ...),其中路径不是字面值- Java — Thymeleaf:
- 控制器方法返回通过字符串拼接构建的视图名称:
return "user/" + var + "/page"或return String.format("prefix/%s/suffix", var)templateEngine.process(var, ctx),其中 var 不是字面值- PHP — Twig:
$twig->createTemplate($var)->render(...)— 参数为任何非字面值的情况$environment->createTemplate($var)- PHP — Smarty:
$smarty->fetch("string:" . $var)或$smarty->display("string:" . $var)$smarty->fetch($var),其中 var 可能包含"string:"前缀- PHP — Blade / Laravel:
Blade::render($var, ...)— 参数为任何非字面值的情况\Illuminate\Support\Facades\View::make($var, ...),其中名称不是字面值(模板路径注入)- Go — text/template 或 html/template:
template.New(name).Parse(var)— Parse 的参数为任何非字面值的情况t.Parse(var),其中 t 是任意模板变量t.ParseFiles(var),其中 var 不是字面值(模板路径注入)- C# — Scriban / Handlebars.Net / DotLiquid / Fluid:
Template.Parse(var)(Scriban)— 非字面值Handlebars.Compile(var)— 非字面值DotLiquid.Template.Parse(var)— 非字面值FluidParser.TryParse(var, ...)— 非字面值跳过内容(安全模式 — 不要标记):
- 第一个参数为字符串字面值的调用:
render_template_string("<h1>Hello</h1>")、ejs.render("<p>static</p>", ctx)- 从可信常量加载文件路径,且用户输入仅出现在上下文中的调用:
render_template("profile.html", user=user_obj)- 不会渲染用户提供内容的模板引擎配置调用:
env = Environment(loader=FileSystemLoader("templates/"))输出格式 — 写入
sast/ssti-recon.md:# SSTI Recon:[项目名称] ## 摘要 发现 [N] 处模板引擎将动态(非字面值)字符串作为模板进行渲染的位置。 ## 候选渲染位置 ### 1. [描述性名称 — 例如“/greet 端点中的 render_template_string”] - **文件**:`path/to/file.ext`(第 X-Y 行) - **函数 / 端点**:[函数名称或路由] - **模板引擎**:[Jinja2 / EJS / Handlebars / FreeMarker / Twig / ERB 等] - **渲染调用**:[render_template_string / from_string / ejs.render / Handlebars.compile 等] - **动态参数**:`var_name` — [简要说明其可能代表的内容,例如“看起来来自表单字段”或“来源未知”] - **代码片段**:[带有动态参数的渲染调用]
[每个位置重复一次]
阶段 1 之后:继续前先检查候选项
阶段 1 完成后,读取 sast/ssti-recon.md。如果侦察发现零个候选渲染位置(摘要显示“Found 0”,或者“Candidate Rendering Sites”部分为空或不存在),则完全跳过阶段 2 和阶段 3。改为将以下内容写入 sast/ssti-results.md,然后停止:
# SSTI Analysis Results
No vulnerabilities found.
只有在阶段 1 至少发现一个候选渲染位置时,才继续进行阶段 2。
阶段 2:验证——追踪用户输入(分批处理)
阶段 1 完成后,读取 sast/ssti-recon.md,并将候选渲染位置拆分为每批最多 3 个候选项。并行为每批启动一个子 Agent。每个子 Agent 仅追踪分配给自己的候选项的污点,并将结果写入各自的批次文件。
分批流程(由你,即编排者,执行,而非子 Agent):
- 读取
sast/ssti-recon.md,统计“Candidate Rendering Sites”下编号候选项部分的数量(### 1.、### 2.等)。 - 将候选项分成每批最多 3 个。例如,8 个候选项 → 3 批(1-3、4-6、7-8)。
- 从侦察文件中提取每批候选项部分的完整文本。
- 并行启动所有批次子 Agent,并仅向每个子 Agent 提供分配给它的候选项。
- 每个子 Agent 将结果写入
sast/ssti-batch-N.md,其中 N 是从 1 开始的批次编号。 - 从
sast/architecture.md中确定项目的主要语言和框架,并仅选择上文“易受攻击与安全示例”部分中匹配的示例。例如,如果项目使用 Python/Flask 和 Jinja2,则仅包含“Python — Flask / Jinja2”示例。在下方[TECH-STACK EXAMPLES]所示位置,将选出的示例加入每个子 Agent 的指令中。
向每个批次子 Agent 提供以下指令(替换为对应批次的值):
目标:针对每个分配的候选渲染位置,判断用户提供的值是否会到达动态模板字符串参数。我们的目标是查找 SSTI 漏洞。将结果写入
sast/ssti-batch-[N].md。分配给你的候选项(来自侦察阶段):
[在此粘贴分配的候选项完整内容,并保留原始编号]
上下文:你将获得项目的架构摘要。请利用它了解请求入口点、中间件以及数据在应用程序中的流向。
SSTI 参考——需要追踪的内容:
对于每个渲染位置,沿着动态模板参数反向追踪其来源。
- 直接用户输入——参数直接来自请求源,中间没有经过转换:
- HTTP 查询参数:
request.GET.get(...)、req.query.x、params[:x]、$_GET['x']、c.Query("x")- 路径参数:
request.path_params['id']、req.params.id、params[:id]- 请求正文/表单字段:
request.POST.get(...)、req.body.x、params[:x]、$_POST['x']- HTTP 标头:
request.headers.get(...)、req.headers['x']- Cookie:
request.COOKIES.get(...)、req.cookies.x- 文件上传内容:如果读取了文件内容并将其作为模板字符串传递
- 间接用户输入——参数由用户输入经过转换、函数调用或中间赋值而来。请追踪完整链路:
- 变量被赋为函数的返回值 → 检查该函数的参数来源
- 变量作为函数参数传递 → 检查调用位置
- 从类属性或其他位置设置的共享状态中读取变量 → 查找设置该值的位置
- 根据条件为变量赋值——检查所有分支
- 二阶输入——模板字符串从数据库、配置存储或文件中读取,但存储的值最初来自用户输入(例如用户提交的“自定义邮件模板”功能):
- 查找此值的写入位置——它是否来自用户提供的字段?
- 存储前是否进行了清理?注意:清理 SSTI 载荷并不可靠——仍应标记。
- 服务端值/硬编码值——模板字符串来自启动时加载的文件、硬编码常量或不受用户影响的服务端逻辑——此位置不可利用。
模板引擎风险等级:
- 严重:Jinja2、Mako、Twig、Smarty、FreeMarker、Velocity、ERB、Pug、EJS、Go
text/template、Thymeleaf——可能实现完整的代码执行- 高:Handlebars(搭配原型污染利用链时)、Nunjucks、Lodash
_.template、Blade、Razor- 中/逻辑无关型:Mustache、Liquid(未启用危险标签时)——通常无法任意执行代码,但仍需检查是否会泄露数据
需要检查的缓解措施:
- 模板引擎是否运行在沙箱模式下?(例如 Jinja2
SandboxedEnvironment、启用严格策略的 Twigsandbox扩展)- 输入在用作模板前是否经过验证或过滤?注意:基于黑名单过滤模板语法字符(
{、}、%)并非可靠的缓解措施——攻击者通常可以绕过。- 渲染结果是直接传给响应,还是用于非危险的上下文?
此项目技术栈中的易受攻击与安全示例:
[TECH-STACK EXAMPLES]
分类:
- 易受攻击:有明确证据表明,用户输入在没有有效缓解措施的情况下到达了采用严重/高风险引擎的模板字符串参数。
- 很可能易受攻击:用户输入可能到达模板字符串(间接数据流或二阶输入),或使用了中风险引擎,或仅应用了黑名单过滤。
- 不易受攻击:模板字符串仅来自服务端(文件、常量、硬编码值),或已确认采用正确配置的沙箱。
- 需要人工审查:无法有把握地确定参数来源,或使用了逻辑无关型引擎且数据泄露范围不明确。
输出格式——写入
sast/ssti-batch-[N].md:# SSTI 批次 [N] 结果 ## 发现 ### [易受攻击] 描述性名称 - **文件**:`path/to/file.ext`(第 X-Y 行) - **端点/函数**:[路由或函数名称] - **模板引擎**:[Jinja2 / FreeMarker / Twig / ERB / 等](严重性:严重/高) - **问题**:[例如,“HTTP 查询参数 `tmpl` 直接流入 render_template_string()”] - **污点追踪**:[从入口点到渲染调用的逐步追踪——例如,“request.args.get('tmpl') → tmpl → render_template_string(tmpl)”] - **影响**:远程代码执行——攻击者可执行任意操作系统命令、读取文件、窃取机密或横向移动到内部系统。 - **概念验证载荷**:[适用于该引擎的模板语法载荷。 Jinja2 示例:?tmpl={{config.class.init.globals['os'].popen('id').read()}} FreeMarker 示例:?tmpl=<#assign+ex="freemarker.template.utility.Execute"?new()>${ex("id")} Twig 示例:?tmpl={{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}} ERB 示例:?tmpl=<%=
id%> EJS 示例:?tmpl=<%- global.process.mainModule.require('child_process').execSync('id') %>]- **修复建议**:切勿将用户输入用作模板字符串。应将用户数据作为上下文变量传给静态模板。如果产品需求必须使用动态模板,请使用沙箱化的逻辑无关型引擎(例如 Mustache、采用安全配置的 Liquid),并严格验证输入。 ### [很可能易受攻击] 描述性名称 - **文件**:`path/to/file.ext`(第 X-Y 行) - **端点/函数**:[路由或函数名称] - **模板引擎**:[引擎名称](严重性:高/中) - **问题**:[例如,“模板字符串可能经由辅助函数从用户输入获取”或“二阶输入:用户提交的模板存储在数据库中,随后在服务端求值”] - **污点追踪**:[尽可能追踪,并标明不确定的步骤] - **风险说明**:[说明仍存在风险的原因——例如,“二阶 SSTI:用户可在提交时构造载荷,在模板稍后渲染时触发执行”] - **概念验证载荷**:[适用于该引擎的载荷]
- **修复建议**:[具体修复方法] ### [不易受攻击] 描述性名称 - **文件**:`path/to/file.ext`(第 X-Y 行) - **端点/函数**:[路由或函数名称] - **原因**:[例如,“模板字符串从硬编码文件路径加载”或“已确认使用带有限制全局变量的 Jinja2 SandboxedEnvironment”] ### [需要人工审查] 描述性名称 - **文件**:`path/to/file.ext`(第 X-Y 行) - **端点/函数**:[路由或函数名称] - **不确定之处**:[无法确定参数来源的原因] - **建议**:[需要手动追踪的内容——例如,“检查 services/email.py 中的 `get_custom_template()`,确认其返回值的来源”]
阶段 3:合并 — 汇总批次结果
等待阶段 2 的所有批次子 Agent 完成后,读取每个 sast/ssti-batch-*.md 文件,并将它们合并为一个 sast/ssti-results.md。由你(编排器)直接完成此操作,无需子 Agent。
合并步骤:
- 读取所有
sast/ssti-batch-1.md、sast/ssti-batch-2.md等文件。 - 收集每个批次文件中的所有发现并合并为一个列表,同时保留原始分类和所有详细字段。
- 统计所有批次的总数,用于执行摘要。
- 按以下格式将合并后的报告写入
sast/ssti-results.md:
# SSTI Analysis Results: [Project Name]
## Executive Summary
- Rendering sites analyzed: [total across all batches]
- Vulnerable: [N]
- Likely Vulnerable: [N]
- Not Vulnerable: [N]
- Needs Manual Review: [N]
## Findings
[All findings from all batches, grouped by classification:
VULNERABLE first, then LIKELY VULNERABLE, then NEEDS MANUAL REVIEW, then NOT VULNERABLE.
Preserve every field from the batch results exactly as written.]
- 写入
sast/ssti-results.md后,删除所有中间批次文件(sast/ssti-batch-*.md)。
重要提醒
- 读取
sast/architecture.md,并将其内容作为上下文传递给所有子 Agent。 - 阶段 2 必须在阶段 1 完成后运行,因为它依赖侦察输出。
- 阶段 3 必须在阶段 2 的所有批次完成后运行,因为它依赖所有批次输出。
- 每个子 Agent 的批次大小为 3 个候选项。如果候选项总数为 1–3 个,则使用一个子 Agent。如果有 10 个,则使用 4 个子 Agent(3+3+3+1)。
- 并行启动所有批次子 Agent,不要按顺序运行。
- 每个批次子 Agent 只接收分配给它的候选项文本(来自侦察文件),而不是整个侦察文件。这样可以让每个子 Agent 的上下文保持精简、聚焦。
- 阶段 1 纯粹是结构分析:标记任何用作模板字符串参数的动态(非字面量)变量。阶段 1 不要尝试追踪用户输入;这是阶段 2 的工作。
- 阶段 2 纯粹是污点分析:对于分配给某批次的每个位置,追踪动态模板参数的来源。如果它来自用户可控来源,则该位置属于真实漏洞。
- 关键区别在于模板字符串与模板上下文:作为
render_template("page.html", user=input)中的变量名/值传入的用户输入是安全的。作为render_template_string(input)中的模板字符串本身传入的用户输入则很危险。 - 二阶 SSTI 很容易被漏掉:所谓的“自定义模板”功能可能会让用户在数据库中存储 Jinja2/Twig 语法。如果之后在未进行沙箱处理的情况下从数据库加载该模板并在服务器端渲染,就属于 SSTI。在阶段 2 中,将从数据库读取的模板字符串视为可能受污染。
- Thymeleaf 片段表达式:在 Spring Boot 中,如果控制器返回由用户输入构造的视图名称(例如,
return "user/" + lang + "/view"),Thymeleaf 可能会处理路径片段中嵌入的 Spring EL 表达式,从而导致 RCE。标记任何使用用户提供的值构造视图名称字符串的控制器。 - 黑名单过滤并非缓解措施:从用户输入中剥除
{{、}}、<%、%>等内容的尝试,常常会被编码、替代语法或嵌套表达式绕过。不要仅仅因为存在过滤,就将发现归类为“无漏洞”。 - 如有疑问,应归类为“需要人工审查”,而非“无漏洞”。在安全评估中,漏报比误报更糟糕。
- 对所有“有漏洞”和“可能有漏洞”的发现,提供适用于相应引擎的概念验证载荷。载荷应先使用数学表达式(例如
{{7*7}})测试并确认模板确实执行,然后再升级到 RCE 载荷。 - 清理中间文件:写入最终的
sast/ssti-results.md后,删除sast/ssti-recon.md和所有sast/ssti-batch-*.md文件。