FlawOpen

flawopen.com/xss/Python

● CWE-79 · 高
セキュリティ研究 · FlawOpen

Python のクロスサイトスクリプティング(XSS)対策(Flask・Jinja2・Django)

ユーザー入力を Markup や mark_safe で包んだり、render_template_string に渡す f-string に入れたりすると、訪問者の文章が他のユーザーのブラウザーで、そのユーザーのセッションのまま JavaScript として実行されます(CWE-79)。Flask・Jinja2・Django が自動でエスケープする箇所、しない 4 つの箇所、リッチテキストを安全に許可する方法を解説します。

💡 わかりやすい解説 (ELI5)

保護された入力欄のある印刷所を想像してください。空欄に書かれた文字は普通の文字として印刷されます。しかし、印刷前に店員が客のメモをプリンターの指示書へ貼り付けると、そのメモが印刷命令になり得ます。render_template_string に渡す Python の f-string は同じことをします。入力を Jinja2 のソースへ埋め込み、autoescape が変数として処理する前に解釈対象へ入れてしまいます。Markup と mark_safe は「確認済み HTML」のスタンプのようなものですが、内容を確認しません。入力はテンプレート変数のままにして autoescape に任せ、小さな Django の断片には format_html を使います。

主要な概念と専門用語

Jinja2 autoescape
Flask は .html、.htm、.xml、.xhtml のテンプレートと render_template_string で Jinja2 の自動エスケープを有効にするため、{{ q }} は <img ...> になります。単独で作った jinja2.Environment() は、autoescape=select_autoescape() を渡さない限り自動エスケープが無効です。
Markup / |safe / mark_safe
3 つとも文字列を安全な HTML としてマークし、テンプレートエンジンはそれをエスケープしなくなります。これらは開発者による約束であってサニタイザーではありません。Markup(user_input) や mark_safe(user_input) は、ユーザーが入力したものをそのまま公開します。
render_template_string(f"...")
f-string は Jinja2 が解析する前に値をテンプレートのソースに入れてしまうため、自動エスケープはそれを変数として扱いません。同じ行はサーバーサイドテンプレートインジェクションでもあり、{{ 7*7 }} のような値は Jinja2 が評価します。
django.utils.html.format_html
str.format のように HTML を組み立てますが、先に各引数を conditional_escape でエスケープしてから結果を安全とマークします。mark_safe(f"...") の代わりに、Python コードで小さな HTML 断片を作る Django 推奨の方法です。
nh3.clean()
リッチテキストに必要なのはエスケープではなくサニタイザーです。nh3.clean()(Rust の ammonia)はタグと属性の許可リストを持ち、<script>、イベントハンドラー、javascript: URL を取り除きます。Bleach は非推奨となり、nh3 が保守されている後継です。

フレームワーク対応

ステップ・バイ・ステップの攻撃フロー

ステップ 1

入力がリクエストで届く

検索ボックス、プロフィールフォーム、リンクのパラメータが q、bio、next をサーバーに送ります。被害者に細工したリンクを送ることも、値が保存されてプロフィールを見る全員に表示されることもあります。

ステップ 2

値がエスケープをすり抜ける

f-string がそれをテンプレートのソースに入れるか、Markup() や mark_safe() が信頼済みとマークするか、< と > のエスケープだけでは足りない href に入ります。無害なテスト値 <img src=x onerror=alert(1)> がそのまま通ります。

ステップ 3

ブラウザーがそれをマークアップとして解析する

レスポンスには本物の要素として <img src=x onerror=alert(1)> が含まれます。画像リソースの読み込みに失敗すると onerror が発火し、リンク内の javascript: URL はクリック時に実行されます。

ステップ 4

スクリプトがサイトとして実行される

示された Flask/Jinja と Django の経路では、出力されたマークアップがアプリのオリジンで実行されます。信頼できない値は自動エスケープされるテンプレートデータ位置に置き、HTML を許可するのは文脈に合ったサニタイズ後に限ります。

ソースコード比較:脆弱 vs 堅牢化

脆弱な実装
# views.py: 以下のハンドラーはどれもリクエストの文字列を HTML としてページに入れてしまう
from django.utils.safestring import mark_safe
from flask import Flask, render_template_string, request
from markupsafe import Markup

app = Flask(__name__)


# f-string がテンプレートのソースを作るため、Jinja2 は q をエスケープしない
@app.get("/search")
def search():
    q = request.args.get("q", "")
    return render_template_string(f"<h1>Results for {q}</h1>")


# Markup() が Jinja2 に bio は信頼できる HTML だと伝えてしまう
@app.get("/profile")
def profile():
    bio = request.args.get("bio", "")
    return render_template_string("<div class='bio'>{{ bio }}</div>", bio=Markup(bio))


# f-string に mark_safe を使うと name が HTML として公開される
def greeting(name):
    return mark_safe(f"<p>Hello, {name}</p>")


# 自動エスケープでは href の javascript: URL は止まらない
@app.get("/continue")
def continue_link():
    url = request.args.get("next", "/")
    return render_template_string('<a href="{{ url }}">Continue</a>', url=url)
堅牢化されたセキュアパッチ
# views.py: 値はテンプレート変数のまま。リッチテキストはサニタイズする
import nh3
from django.utils.html import format_html
from flask import Flask, render_template_string, request

app = Flask(__name__)


# q はテンプレート変数なので自動エスケープでエンコードされる
@app.get("/search")
def search():
    q = request.args.get("q", "")
    return render_template_string("<h1>Results for {{ q }}</h1>", q=q)


# HTML を信頼する前に nh3 がタグの許可リストを適用する
@app.get("/profile")
def profile():
    bio = request.args.get("bio", "")
    return render_template_string("<div class='bio'>{{ bio|safe }}</div>", bio=nh3.clean(bio))


# format_html は結果を安全とマークする前に各引数をエスケープする
def greeting(name):
    return format_html("<p>Hello, {}</p>", name)


# リンク先には自サイトのパスだけを許可する
@app.get("/continue")
def continue_link():
    url = request.args.get("next", "/")
    if not url.startswith("/") or url.startswith("//"):
        url = "/"
    return render_template_string('<a href="{{ url }}">Continue</a>', url=url)

エンジニアリング&システム堅牢化チェックリスト

参考資料