sast-ssti について
コードベースのSSTIを調べるセキュリティ評価向けスキルです。動的なテンプレート処理箇所を探し、ユーザー入力をバッチ追跡して結果を統合します。事前に `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)の検出
コードベースにおけるサーバーサイドテンプレートインジェクションの脆弱性を見つけるため、対象を絞ったセキュリティ評価を実施します。このスキルでは、サブエージェントを使った3段階のアプローチを採用します。recon(テンプレート文字列が動的なレンダリング箇所の候補を見つける)、batched verify(ユーザー入力が各箇所のテンプレート引数に到達するかを、3件ずつ並列に追跡する)、merge(各バッチの結果を最終レポートにまとめる)です。
前提条件: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)— Go のtext/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:テンプレートがサニタイズされていないユーザーデータを出力し、それがブラウザーでレンダリングされる場合、これは SSTI ではなく XSS です。
- 許可リストで検証された動的なファイル名を使う静的テンプレート:テンプレート名がユーザー入力に由来していても、許可されたテンプレート名のハードコードされた集合に照らして厳密に検証される場合、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().
実行
このスキルは、サブエージェントを使用して3つのフェーズで実行されます。sast/architecture.md の内容を、すべてのサブエージェントにコンテキストとして渡してください。
フェーズ1:動的文字列を使用するテンプレートレンダリング箇所を見つける
以下の指示を使ってサブエージェントを起動します。
目標:テンプレートエンジンが静的なテンプレートファイルを読み込むのではなく、動的に構築された文字列をテンプレートそのものとしてレンダリング、コンパイル、または評価しているコードベース内の箇所をすべて見つけます。結果は
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.templateの場合)mako.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)— リテラル以外の 4 番目の引数すべて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.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 調査:[プロジェクト名] ## 概要 テンプレートエンジンが動的な(リテラルではない)文字列をテンプレートとしてレンダリングしている箇所が[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 を読みます。偵察で候補となるレンダリング箇所が 0 件だった場合(概要に「Found 0」と報告されているか、「Candidate Rendering Sites」セクションが空、または存在しない場合)、フェーズ 2 とフェーズ 3 を完全にスキップします。代わりに、次の内容を sast/ssti-results.md に書き込み、終了します。
# SSTI Analysis Results
No vulnerabilities found.
フェーズ 1 で候補となるレンダリング箇所が 1 件以上見つかった場合にのみ、フェーズ 2 に進みます。
フェーズ 2:検証 — ユーザー入力を追跡(バッチ処理)
フェーズ 1 が完了したら、sast/ssti-recon.md を読み、候補となるレンダリング箇所を最大 3 件ずつのバッチに分割します。バッチごとにサブエージェントを 1 つずつ並行して起動します。各サブエージェントは、割り当てられた候補のみを対象に汚染を追跡し、それぞれのバッチファイルに結果を書き込みます。
バッチ処理の手順(サブエージェントではなく、あなたがオーケストレーターとして実行します):
sast/ssti-recon.mdを読み、「Candidate Rendering Sites」にある番号付き候補セクション(### 1.、### 2.など)の数を数えます。- 最大 3 件ずつのバッチに分けます。たとえば候補が 8 件の場合、3 つのバッチ(1~3、4~6、7~8)に分けます。
- バッチごとに、偵察ファイルから該当する候補セクションの全文を抽出します。
- すべてのバッチサブエージェントを並行して起動し、それぞれに割り当てられた候補のみを渡します。
- 各サブエージェントは
sast/ssti-batch-N.mdに書き込みます(N は 1 から始まるバッチ番号です)。 sast/architecture.mdからプロジェクトの主要な言語/フレームワークを特定し、上記の「脆弱な例とセキュアな例」セクションから該当する例のみを選択します。たとえば、プロジェクトで Python/Flask と Jinja2 を使用している場合は、「Python — Flask / Jinja2」の例のみを含めます。以下の[TECH-STACK EXAMPLES]の指定箇所に、選択した例を各サブエージェントへの指示に含めます。
各バッチサブエージェントに、次の指示を与えます(バッチ固有の値を置き換えてください):
目標:割り当てられた各候補レンダリング箇所について、ユーザーが指定した値が動的なテンプレート文字列の引数に到達するかどうかを判断します。目標は 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 ペイロードのサニタイズは信頼できません — それでも脆弱性として報告してください。
- サーバー側/ハードコードされた値 — テンプレート文字列が、起動時に読み込まれるファイル、ハードコードされた定数、またはユーザーの影響を受けないサーバー側のロジックに由来する場合、この箇所は悪用できません。
テンプレートエンジンのリスクレベル:
- Critical:Jinja2、Mako、Twig、Smarty、FreeMarker、Velocity、ERB、Pug、EJS、Go の
text/template、Thymeleaf — 完全なコード実行が可能です- High:Handlebars(プロトタイプ汚染ガジェットがある場合)、Nunjucks、Lodash
_.template、Blade、Razor- Medium/ロジックレス:Mustache、Liquid(危険なタグが有効になっていない場合) — 任意コード実行は通常できませんが、データ漏えいがないか確認してください
確認する緩和策:
- テンプレートエンジンはサンドボックスモードで実行されていますか?(例:Jinja2 の
SandboxedEnvironment、厳格なポリシーを設定した Twig のsandbox拡張機能)- 入力はテンプレートとして使用される前に検証またはフィルタリングされていますか?注意:テンプレート構文文字(
{、}、%)をブロックリスト方式でフィルタリングしても、信頼できる緩和策にはなりません — 攻撃者はしばしば回避できます。- レンダリング結果はレスポンスに直接渡されますか、それとも危険性のないコンテキストで使用されますか?
このプロジェクトの技術スタックにおける脆弱な例と安全な例:
[TECH-STACK EXAMPLES]
分類:
- 脆弱:ユーザー入力が、有効な緩和策なしに、クリティカル/高リスクのエンジンのテンプレート文字列引数に到達することが実証されている
- 脆弱の可能性あり:ユーザー入力がテンプレート文字列に到達する可能性が高い(間接的なフローまたは二次的な入力)、または中リスクのエンジンが使用されている、またはブロックリストによるフィルタリングしか適用されていない
- 脆弱ではない:テンプレート文字列がサーバー側のみ(ファイル、定数、ハードコード)に由来する、または適切に設定されたサンドボックスが確認されている
- 手動レビューが必要:引数の発生元を確実に特定できない、またはロジックレスエンジンが使われており、データ漏えいの範囲が不明確
出力形式 —
sast/ssti-batch-[N].mdに記述してください:# SSTI バッチ [N] の結果 ## 検出結果 ### [脆弱] 説明的な名前 - **ファイル**:`path/to/file.ext`(行 X-Y) - **エンドポイント/関数**:[ルートまたは関数名] - **テンプレートエンジン**:[Jinja2 / FreeMarker / Twig / ERB / など](深刻度:Critical/High) - **問題**:[例:「HTTP クエリパラメーター `tmpl` が `render_template_string()` に直接渡される」] - **汚染トレース**:[エントリーポイントからレンダリング呼び出しまでの手順 — 例:「request.args.get('tmpl') → tmpl → render_template_string(tmpl)」] - **影響**:リモートコード実行 — 攻撃者は任意の OS コマンドの実行、ファイルの読み取り、シークレットの持ち出し、または内部ネットワークへの侵入が可能です。 - **概念実証ペイロード**:[エンジンに適したテンプレート構文のペイロード。 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) - **エンドポイント/関数**:[ルートまたは関数名] - **テンプレートエンジン**:[エンジン名](深刻度:High/Medium) - **問題**:[例:「ヘルパー関数を介してテンプレート文字列がユーザー入力に由来する可能性が高い」または「二次的な脆弱性:ユーザーが投稿したテンプレートがデータベースに保存され、後にサーバー側で評価される」] - **汚染トレース**:[不確かな箇所を明示した、可能な範囲でのトレース] - **懸念点**:[依然としてリスクとなる理由 — 例:「二次的な SSTI:ユーザーが投稿時にペイロードを仕込み、後でテンプレートがレンダリングされる際に実行させることが可能」] - **概念実証ペイロード**:[エンジン用のペイロード]
- **修正方法**:[具体的な修正方法] ### [脆弱ではない] 説明的な名前 - **ファイル**:`path/to/file.ext`(行 X-Y) - **エンドポイント/関数**:[ルートまたは関数名] - **理由**:[例:「テンプレート文字列はハードコードされたファイルパスから読み込まれる」または「Jinja2 SandboxedEnvironment が制限されたグローバルとともに使用されていることを確認済み」] ### [手動レビューが必要] 説明的な名前 - **ファイル**:`path/to/file.ext`(行 X-Y) - **エンドポイント/関数**:[ルートまたは関数名] - **不確実な点**:[引数の発生元を特定できなかった理由] - **提案**:[手動で追跡すべき対象 — 例:「`services/email.py` の `get_custom_template()` をたどり、戻り値の発生元を確認する」]
フェーズ 3: マージ — バッチ結果の統合
すべてのフェーズ 2 のバッチサブエージェントが完了したら、すべての sast/ssti-batch-*.md ファイルを読み込み、それらを単一の sast/ssti-results.md にマージします。これ (オーケストレーター) は直接実行します — サブエージェントは不要です。
マージ手順:
- すべての
sast/ssti-batch-1.md、sast/ssti-batch-2.md、... ファイルを読み込みます。 - 各バッチファイルからすべての検出結果を収集し、元の分類とすべての詳細フィールドを保持したまま、1 つのリストに統合します。
- エグゼクティブサマリー用に、すべてのバッチにわたる合計を集計します。
- 次の形式を使用して、マージされたレポートを
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を読み込み、その内容をコンテキストとしてすべてのサブエージェントに渡してください。- フェーズ 2 はフェーズ 1 の完了後に実行する必要があります — これは偵察の出力に依存します。
- フェーズ 3 はすべてのフェーズ 2 バッチの完了後に実行する必要があります — これはすべてのバッチ出力に依存します。
- バッチサイズは サブエージェントあたり 3 候補です。合計で 1〜3 候補の場合は、単一のサブエージェントを使用します。10 候補の場合は、4 つのサブエージェントを使用します (3+3+3+1)。
- すべてのバッチサブエージェントを並列で起動してください — 順次実行しないでください。
- 各バッチサブエージェントは、偵察ファイル全体ではなく、割り当てられた候補のテキストのみを受け取ります。これにより、各サブエージェントのコンテキストを小さく焦点を絞った状態に保ちます。
- フェーズ 1 は純粋に構造的です: テンプレート文字列引数として使用される動的 (非リテラル) 変数をすべてフラグ付けします。フェーズ 1 でユーザー入力を追跡しようとしないでください — それはフェーズ 2 の役割です。
- フェーズ 2 は純粋にテイント解析です: バッチに割り当てられた各サイトについて、動的テンプレート引数をその起源まで追跡します。それがユーザー制御のソースから来ている場合、そのサイトは実際の脆弱性です。
- 重要な区別は テンプレート文字列とテンプレートコンテキストです:
render_template("page.html", user=input)内で変数名/値として渡されるユーザー入力は安全です。render_template_string(input)にテンプレート文字列自体として渡されるユーザー入力は危険です。 - 二次的 SSTI は見落とされやすいです: 「カスタムテンプレート」機能により、ユーザーが Jinja2/Twig 構文をデータベースに保存できる場合があります。その保存されたテンプレートが後でサンドボックス化なしにサーバー側で読み込まれ、レンダリングされると、それは SSTI です。フェーズ 2 では、DB から読み込まれたテンプレート文字列を潜在的にテイントされたものとして扱ってください。
- Thymeleaf フラグメント式: Spring Boot では、コントローラーがユーザー入力から構築されたビュー名を返す場合 (例:
return "user/" + lang + "/view")、Thymeleaf がパスセグメントに埋め込まれた Spring EL 式を処理し、RCE が可能になることがあります。ユーザー提供の値を使用してビュー名文字列を構築するコントローラーをすべてフラグ付けしてください。 - ブロックリストフィルタリングは緩和策ではありません: ユーザー入力から
{{、}}、<%、%>などを除去しようとする試みは、エンコーディング、代替構文、またはネストされた式を介して日常的にバイパスされます。フィルタリングが存在するという理由だけで、検出結果を「脆弱ではない」と分類しないでください。 - 疑わしい場合は、「脆弱ではない」ではなく「手動レビューが必要」と分類してください。セキュリティ評価では、偽陰性は偽陽性よりも悪いです。
- すべての「脆弱」および「脆弱の可能性が高い」検出結果には、エンジンに適した概念実証ペイロードを含めてください。ペイロードは、RCE ペイロードにエスカレートする前に、まず数学式 (例:
{{7*7}}) でテストしてテンプレート実行を確認する必要があります。 - 中間ファイルをクリーンアップします: 最終的な
sast/ssti-results.mdが書き込まれた後、sast/ssti-recon.mdとすべてのsast/ssti-batch-*.mdファイルを削除します。