セキュリティ

認可を設計する — 他人のデータが見える穴を作らない

脆弱性診断で最も多く、そして最も重大なのが認可の不備です。
「URLのIDを変えたら他人のデータが見えた」という類のものです。

なぜ起きるか

認証(ログインしているか)は仕組みで守られますが、
認可(そのデータを見てよいか)は毎回書く必要があるためです。

# × ログインチェックだけ
@login_required
def order_detail(request, pk):
    order = Order.objects.get(pk=pk)     # 誰の注文でも取れてしまう
    return render(request, "detail.html", {"order": order})

書き忘れが1か所でもあれば、そこが穴になります。

構造で防ぐ:常に絞り込んだ集合から取る

個別にチェックを書くのではなく、
そもそも自分のデータしか含まれない QuerySet から取るようにします。

def owned_orders(user):
    return Order.objects.filter(user=user)

@login_required
def order_detail(request, pk):
    order = get_object_or_404(owned_orders(request.user), pk=pk)
    ...

こうすると、チェックを書き忘れる余地がなくなります
他人のIDを指定すれば 404 になります。

403 ではなく 404 を返す

# 403「権限がありません」→ そのIDのデータは存在すると教えている
# 404「見つかりません」  → 存在の有無すら分からない

存在を教えないほうが安全です。

一覧・更新・削除も同じ集合から

詳細画面だけ対策して、更新や削除で漏れるのがよくあるパターンです。

def order_update(request, pk):
    order = get_object_or_404(owned_orders(request.user), pk=pk)   # 同じ関数を使う

「取得する経路をすべて1つの関数に集約する」と、監査もしやすくなります。

推測しにくいIDにする

連番IDは、総当たりで探索されます。
公開URLに使うなら、推測困難な識別子を併用します。

public_id = models.CharField(max_length=22, unique=True, default=gen_token)

ただしこれは認可の代わりにはなりません
「推測しにくい」だけで、漏れたら誰でもアクセスできます。必ず認可と併用します。

一括操作に注意

チェックボックスで選んだ複数件を処理する画面は、危険が集中します。

ids = request.POST.getlist("ids")
# × Order.objects.filter(pk__in=ids).delete()
# ○ 自分のものだけに絞ってから
owned_orders(request.user).filter(pk__in=ids).delete()

API も同じ

class OrderViewSet(viewsets.ModelViewSet):
    def get_queryset(self):
        return Order.objects.filter(user=self.request.user)

get_queryset で絞れば、詳細・更新・削除すべてに効きます。

テストで固定する

認可は必ずテストを書く領域です。

def test_cannot_access_others_order(self):
    self.client.force_login(self.user_b)
    for method, url in [("get", f"/orders/{self.order_a.pk}/"),
                        ("post", f"/orders/{self.order_a.pk}/delete/")]:
        with self.subTest(url=url):
            res = getattr(self.client, method)(url)
            self.assertIn(res.status_code, (403, 404))

まとめ

  • 個別チェックではなく、絞り込んだ集合から取得する構造にする
  • 取得経路を1つの関数に集約する
  • 403 ではなく 404 を返す
  • 一括操作は絞り込んでから実行する
  • 推測しにくいIDは補助であって、認可の代わりではない
  • 認可は必ずテストで固定する