セキュリティ

SQLインジェクションと XSS を「起きない構造」にする

「気をつける」で防ぐのは限界があります。
そもそも危険な書き方ができない構造にするのが確実です。

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 を最後の防波堤として入れる