一覧SD-10-2 · 解釈系への入力

テンプレートインジェクション対策

出力のエスケープと、サーバーで式を評価させない設計の違いを説明します。

説明

テンプレートは、アプリが用意したひな型へ値を埋め込むための仕組みです。入力をひな型そのものへ連結すると、値に見えた文字列が式として実行される場合があります。

Jinjaで同じ文字列をテンプレート本体と変数の値へ渡し、出力を比較します。HTMLエスケープ、関数の呼出し、動的なPython式の評価まで追い、各機能の境界を確認します。

出力のエスケープと、サーバーで式を評価させない設計の違いを説明します。

1固定テンプレートへ値を渡す

アプリ側のひな型を固定し、nameだけを変数として渡します。

from sd10 import * env = Environment(autoescape=True,undefined=StrictUndefined) template = env.from_stringアプリの固定テンプレートを解析する("<p>{{ name }}</p>") print(template.render値だけを渡して出力する(name="alice"))

aliceがp要素内へ埋め込まれます。テンプレートの構造はアプリ側で定めています。

2式の文字列を値として渡す

nameを{{ 7 * 7 }}へ変えます。

text = "{{ 7 * 7 }}" print(template.render(name=text式に見える文字列を値として埋め込む))

式はそのまま表示され、49にはなりません。変数へ渡した文字列を、テンプレートとしてもう一度解釈していないためです。

3同じ文字列をテンプレート本体へ連結する

値をひな型へ直接連結した場合と比較します。

print(env.from_string("<p>"+text+"</p>"入力をテンプレート本体へ含める).render())

49になりました。autoescape=Trueでも、テンプレート内の計算式はサーバー側で評価されます。

4HTMLエスケープの作用を確かめる

HTMLに意味を持つ文字列を、固定テンプレートの変数へ入れます。

print(template.render(name="<em>alice</em>"HTMLタグに見える値を渡す))

山括弧がエンティティへ変換されます。HTML本文で文字を表示するための処理であり、前段でテンプレートに混入した式の実行を止める機能ではありません。

5テンプレートから呼べる機能を確認する

公開試料の関数をひな型へ渡し、呼出し履歴を取ります。

calls = [] def mark(): calls.append関数の副作用を記録する("called") return "marker" print(env.from_string("{{ mark() }}").render(mark=mark)テンプレートへ関数を渡す) print(calls)

markerが出力され、calledが記録されます。テンプレートへ渡すオブジェクトや関数も、そのテンプレートが操作できる範囲を広げます。

6固定テンプレートでは呼出し文字列のまま保持する

同じ関数名を含む文字列をnameへ渡します。

calls.clear() print(template.render(name="{{ mark() }}"呼出しを含む文字列をデータとして渡す,mark=mark)) print(calls)副作用が起きていないことを確認する

文字列が残り、履歴は空です。固定テンプレートの変数として渡すことで、入力からの関数呼出しを防いでいます。

7計算式の入力が不要ならデータとして解析する

数量を受け取る機能で、入力をevalする必要はありません。固定の比較例を使います。

print(eval("7 * 7"固定式の評価を比較する,{"__builtins__":{}},{})) print(json.loads("49")) try:print(json.loads("7 * 7")データパーサーでは式を受け付けないことを確認する) except json.JSONDecodeError:print("not_json")

evalは計算し、JSONは数値49を読みますが式はnot_jsonです。builtinsを空にするだけを汎用の安全なevalとして扱わず、必要な型と処理をアプリ側で定めます。

まとめ

入力は固定テンプレートの変数へ渡します。

HTMLエスケープとサーバー側の式評価は異なる処理です。

必要がない機能へ任意の式評価を持ち込みません。

操作履歴と解説を振り返れます。

Python 3.15 / Jinja 3.1.6

1
from sd10 import * env = Environment(autoescape=True,undefined=StrictUndefined) template = env.from_string("<p>{{ name }}</p>") print(template.render(name="alice"))
<p>alice</p>
2
text = "{{ 7 * 7 }}" print(template.render(name=text))
<p>{{ 7 * 7 }}</p>
3
print(env.from_string("<p>"+text+"</p>").render())
<p>49</p>
4
print(template.render(name="<em>alice</em>"))
<p>&lt;em&gt;alice&lt;/em&gt;</p>
5
calls = [] def mark(): calls.append("called") return "marker" print(env.from_string("{{ mark() }}").render(mark=mark)) print(calls)
marker
['called']
6
calls.clear() print(template.render(name="{{ mark() }}",mark=mark)) print(calls)
<p>{{ mark() }}</p>
[]
7
print(eval("7 * 7",{"__builtins__":{}},{})) print(json.loads("49")) try:print(json.loads("7 * 7")) except json.JSONDecodeError:print("not_json")
49
49
not_json

演習完了