サーバーサイドテンプレートインジェクション

信頼できない入力を実行可能なサーバー側テンプレートにしない

説明

サーバーサイドテンプレートインジェクション(SSTI)は、入力値が通常のデータではなく、実行可能なテンプレートソースとしてコンパイル・評価される問題です。攻撃者は指示や式を記述し、公開されたオブジェクト、メソッド、ヘルパー、アプリケーションデータを利用できます。

次の三つを区別してください。

  • テンプレートソース: エンジンが構文として解釈する本文です。リクエスト値から作るとSSTIにつながります。
  • テンプレート識別子: サーバーが管理するテンプレートを選ぶ名前やパスです。不正な選択やパストラバーサルの原因になり得ますが、その値自体がソースとは限りません。
  • モデル・コンテキストの値: 固定テンプレートに渡すデータです。利用者の入力はこの形で渡してください。

例えば、通常のファイルローダーを使うConfiguration.getTemplate(userInput)はテンプレート名を選びますが、new Template("dynamic", userInput, configuration)は値をFreeMarkerのソースとして解析します。文字列をソースとして解釈するローダーを使う場合も、その文字列を信頼できる必要があります。

想定される影響

  • テンプレートに公開されたデータやアプリケーションオブジェクトの参照
  • 登録済みヘルパーやアクセス可能なメソッドによる機能の呼び出し
  • ループ、再帰、大量出力によるCPU・メモリの枯渇
  • エンジン機能、公開オブジェクト、プロセス権限が許す場合のコード・OSコマンド実行
  • 適切なエンコードなしに結果をHTMLへ挿入する場合のXSS

どのエンジンや設定でも直ちにリモートコードを実行できるわけではありません。影響は言語機能、ヘルパー、オブジェクトへのアクセス方針、データモデル、ファイル・ネットワーク権限によって変わります。

対処方法

  1. テンプレートソースは、信頼できる固定リソースやサーバー管理の定数だけから取得してください。
  2. リクエスト値をソースに連結せず、モデルやコンテキストのデータとして渡してください。
  3. HTML、JavaScript、URLなど出力先に合うエンコードを別途適用してください。これはXSSを抑える対策であり、信頼できないテンプレートソースを安全にするものではありません。
  4. 利用者がテンプレートを書く機能は高リスクとして扱ってください。公開するオブジェクトやヘルパーを減らし、エンジン固有の制限を使い、権限とCPU・メモリ・時間を制限した別プロセスでレンダリングしてください。

エンジンのセキュリティ設定を万能なサンドボックスと考えないでください。

  • VelocityのSecureUberspectorは出発点ですが、アプリケーションがコンテキストに渡すすべての危険なオブジェクトをエンジンが予測することはできません。
  • FreeMarkerで信頼できないテンプレートが不可欠なら、公式FAQに従いTemplateClassResolver.ALLOWS_NOTHING_RESOLVERや、さらに厳しい許可方針を検討してください。SAFER_RESOLVERだけでは不十分で、FreeMarker自体はCPUやメモリの上限を強制できません。
  • GroovyテンプレートはGroovyコードにコンパイルされます。攻撃者が制御する本文をアプリケーションと同じプロセスで実行しないことを基本としてください。

例

Apache Velocity

変更前

リクエスト値をVTLソースとして評価します。

java
import java.io.StringWriter;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

class InsecureVelocityTemplate {
    void render(HttpServletRequest request) {
        String userTemplate = request.getParameter("template");
        Velocity.evaluate(
            new VelocityContext(),
            new StringWriter(),
            "request-template",
            userTemplate
        );
    }
}

変更後

テンプレートを固定し、リクエスト値をコンテキストのデータに渡します。

java
import java.io.StringWriter;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

class SecureVelocityTemplate {
    void render(HttpServletRequest request) {
        VelocityContext context = new VelocityContext();
        context.put("name", request.getParameter("name"));

        Velocity.evaluate(
            context,
            new StringWriter(),
            "welcome-template",
            "Hello $name"
        );
    }
}

この比較はSSTIの境界だけを示します。結果をHTMLで返す場合、nameにはHTMLコンテキストのエンコードも必要です。

FreeMarker

変更前

リクエスト値をテンプレート本文として解析します。

java
import java.io.StringWriter;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import freemarker.template.Configuration;
import freemarker.template.Template;

class InsecureFreeMarkerTemplate {
    void render(HttpServletRequest request, Configuration configuration) throws Exception {
        String userTemplate = request.getParameter("template");
        Template template = new Template("dynamic", userTemplate, configuration);
        template.process(Map.of(), new StringWriter());
    }
}

変更後

サーバー管理のテンプレートを固定名で読み込み、リクエスト値はデータモデルだけに渡します。

java
import java.io.StringWriter;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import freemarker.template.Configuration;
import freemarker.template.Template;

class SecureFreeMarkerTemplate {
    void render(HttpServletRequest request, Configuration configuration) throws Exception {
        Template template = configuration.getTemplate("welcome.ftl");
        Map<String, Object> model = Map.of("name", request.getParameter("name"));
        template.process(model, new StringWriter());
    }
}

参考資料