「気をつける」で防ぐのは限界があります。
そもそも危険な書き方ができない構造にするのが確実です。
SQLインジェクション
ORM を使っている限り、基本的には安全です。
Order.objects.filter(status=user_input) # 安全(プレースホルダになる)
危ないのは、文字列を組み立てて生SQLを投げる書き方です。
# × 絶対にやらない
cursor.execute(f"SELECT * FROM orders WHERE status = '{user_input}'")
# ○ パラメータで渡す
cursor.execute("SELECT * FROM orders WHERE status = %s", [user_input])
extra() や RawSQL も同様で、文字列連結した瞬間に穴が開きます。
並び替えの列名は特に危険です。パラメータ化できないため、
必ず許可リスト方式にします。
ALLOWED_ORDER = {"created_at", "-created_at", "amount", "-amount"}
order = request.GET.get("order", "-created_at")
if order not in ALLOWED_ORDER:
order = "-created_at"
qs = qs.order_by(order)
XSS
テンプレートエンジンは自動でエスケープします。
問題は、それを明示的に外す書き方です。
{{ user_content }} <!-- 安全(エスケープされる) -->
{{ user_content|safe }} <!-- 危険 -->
{% autoescape off %} <!-- 危険 -->
|safe を使ってよいのは、サーバー側で生成したHTMLだけです。
ユーザー入力に付けてはいけません。
ユーザーにHTMLを許可する場合
Markdown 投稿などでHTMLを許可する必要があるときは、
許可リストでサニタイズします。
import bleach
ALLOWED_TAGS = ["p", "br", "strong", "em", "ul", "ol", "li", "a", "code", "pre"]
ALLOWED_ATTRS = {"a": ["href", "title"]}
clean = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS,
protocols=["http", "https"], strip=True)
禁止リストではなく許可リストにします。
禁止リストは必ず抜けが出ます(<img onerror=...> など)。
protocols の指定も重要で、これが無いと javascript: が通ります。
JavaScript へのデータ受け渡し
<!-- × 文字列に </script> が入ると壊れる -->
<script>var data = "{{ value }}";</script>
<!-- ○ -->
{{ value|json_script:"data" }}
<script>var data = JSON.parse(document.getElementById("data").textContent);</script>
CSP で最後の防波堤を作る
万一 XSS が混入しても、外部スクリプトの実行を止められます。
add_header Content-Security-Policy
"default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'" always;
CSP を効かせるには、インラインスクリプトを排除しておく必要があります。
最初から外部ファイルに分けておくと、後から入れやすくなります。
まとめ
- ORM を使う。生SQLは必ずパラメータ化する
- 並び替えの列名は許可リストで検証する
|safeはユーザー入力に使わない- HTMLを許可するなら許可リストでサニタイズし、プロトコルも制限する
- JS へのデータ受け渡しは
json_script - CSP を最後の防波堤として入れる