コース目次 / 第4章

SSTI とパストラバーサル — 式とパスの越境

ユーザー入力がテンプレートの式として評価されるSSTIを起こし、テンプレとデータを分ける直しを確かめます。capstoneで作ったパストラバーサルを注入の一種として復習します。

第4章 / 全6章目安 約12分この章のゴール: SSTIを起こしてテンプレとデータの分離で直し、パストラバーサルを注入として捉え直す

注入の最後は、入力がテンプレートの式として評価される SSTI(Server-Side Template Injection)と、入力がファイルパスの文法として解釈されるパストラバーサルです。どちらも、これまでと同じ形——データが、コードとして解釈される。

法と倫理ゲート

手を動かす前に

このコースは攻撃の技術に触れます。実習してよい相手は、次の3つだけです。

  • (a) 自分で作った練習用のやられ環境(このサイトが配る自作ターゲット)
  • (b) あなた自身の許可された環境(自分の Thousand SKY・secret-notes-ctf など)
  • (c) CTF 競技の中(規約の範囲で)

実在する第三者のシステムを許可なく触ることは、不正アクセス禁止法に触れます。 このゲートは、その線引きを毎回の入口で確かめるためのものです。詳しくは「セキュリティの法と倫理」へ。

SSTI — 入力が、式になる

多くのテンプレートエンジンは、こんにちは、{{ name }} さん のような雛形に値を埋めて画面を作ります。その内部では、テンプレートを式として評価しています。ここにユーザー入力がテンプレート自体の一部として混ざると、入力が式として実行される——それが SSTI です。最小の再現を作ります。

// vuln-template.js — 学習用「わざと穴を空けた」テンプレ描画。127.0.0.1 だけで動かす。
// 狙う穴: SSTI(ユーザー入力を、評価されるテンプレートの中に埋め込んでいる)。
const http = require('node:http');

const server = http.createServer((req, res) => {
  const url = new URL(req.url, 'http://127.0.0.1:3500');
  const name = url.searchParams.get('name') || 'ゲスト';

  // 【穴: SSTI】入力を、評価されるテンプレート文字列の中に連結している。
  const template = 'こんにちは、' + name + ' さん';
  let rendered;
  try {
    // テンプレートを式(テンプレートリテラル)として評価する = エンジンがやっていること。
    rendered = Function('return `' + template + '`')();
  } catch (e) {
    rendered = 'エラー: ' + e.message;
  }

  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end(rendered);
});

server.listen(3500, '127.0.0.1', () => console.log('やられテンプレ: http://127.0.0.1:3500 (Ctrl+C で停止)'));
node vuln-template.js

正規の動き(http://127.0.0.1:3500/?name=はると → こんにちは、はると さん)を確かめたら、名前に式を入れます。

http://127.0.0.1:3500/?name=${7*7}

返ってきたのは こんにちは、49 さん。あなたが入れた $49 が、文字ではなく式として計算され、49 になった。これが SSTI です。7*7 が計算できるということは、サーバ上で任意の式を評価させられるということ。$v26.3.0 のような式でサーバ内部の情報を引き出したり、さらに踏み込めばコマンド実行に至ったりします(この教材では、危険な完成形の攻撃コードは載せません——式が評価されるという事実だけで、深刻さは十分に分かります)。

SSTI の修正 — テンプレは固定、入力はデータ

原因は、ユーザー入力がテンプレート(コード)の一部になっていること。直しは、これまでと同じ——テンプレートは固定し、入力はそこに“渡す”データとして扱う。

// 直したあと: テンプレートは固定の関数。入力は評価されず、値として連結されるだけ。
const render = (data) => 'こんにちは、' + data.name + ' さん';
res.end(render({ name }));

この name は、もう評価される式ではなく、ただ連結される値です。$49 を入れても こんにちは、$49 さん と文字で出るだけ。実際のテンプレートエンジンでも原則は同じ——ユーザー入力を、テンプレートの“ソース”に混ぜない。テンプレートに“渡す変数”として与える。データとコードを分ける、いつもの一手です。

再起動して ?name=${7*7} をもう一度開けば、今度は こんにちは、${7*7} さん。式は評価されません。

パストラバーサルも、注入の仲間

capstone(sec-make-target)で作ったパストラバーサルを、ここで捉え直します。あれは ../ を使って公開フォルダの外のファイルを読み出す穴でした。

実はこれも注入の一種です——入力が、ファイルパスの“文法”(../ という上位ディレクトリ指定)として解釈される。HTML・SQL・シェル・テンプレートと同じ形が、ファイルパスの層で起きているだけ。
だから直しも同じ発想でした:「.. という文字を消す」(ブラックリスト)ではなく、「連結後の実パスが、公開フォルダの中に収まっているものだけ許す」(許可リスト)。path.resolve で解決してから基準ディレクトリ配下かを確かめる、あの直しです。

未見の人は capstone の「自分だけの脆弱ターゲットを作る」で、実際に作って・攻撃して・直せます。

持ち帰る一言

SSTI も、テンプレとデータを分ければ止まる。パストラバーサルも注入の仲間。 ユーザー入力をテンプレートのソースに混ぜず、渡す変数として与える。パスは許可リストで基準の中に閉じる。層は違っても、直しはいつも「データとコードを分ける」。次はまとめて、五つの穴を一つの原則に畳みます。

こうなっていればOK

卒業まであと1章です。

この章はまだ完了していません。