テンプレートインジェクション対策
出力のエスケープと、サーバーで式を評価させない設計の違いを説明します。
説明
テンプレートは、アプリが用意したひな型へ値を埋め込むための仕組みです。入力をひな型そのものへ連結すると、値に見えた文字列が式として実行される場合があります。
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
from sd10 import *
env = Environment(autoescape=True,undefined=StrictUndefined)
template = env.from_string("<p>{{ name }}</p>")
print(template.render(name="alice"))<p>alice</p>
text = "{{ 7 * 7 }}"
print(template.render(name=text))<p>{{ 7 * 7 }}</p>print(env.from_string("<p>"+text+"</p>").render())<p>49</p>
print(template.render(name="<em>alice</em>"))<p><em>alice</em></p>
calls = []
def mark():
calls.append("called")
return "marker"
print(env.from_string("{{ mark() }}").render(mark=mark))
print(calls)marker ['called']
calls.clear()
print(template.render(name="{{ mark() }}",mark=mark))
print(calls)<p>{{ mark() }}</p>
[]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
演習完了