ログイン確認しても、まだ穴は残っている
個人開発でセキュリティというと、まず「ログイン機能をつける」ことを思い浮かべる人が多いと思います。でも実は、ログインしているかの確認だけでは足りません。
OWASP Top 10 で近年もっとも多いカテゴリは、インジェクションでもXSSでもなく Broken Access Control(アクセス制御の不備)、いわゆる「認可漏れ」です。そして個人開発では、これが本当によく起きます。
この記事では、その代表格である IDOR(Insecure Direct Object Reference) を、悪い例と直し方のコードつきで解説します。
「認証」と「認可」は別物
まず言葉の整理から。
- 認証(Authentication):あなたが誰か?(ログインしているか)
- 認可(Authorization):あなたはこの操作・このデータにアクセスしてよいか?
ログイン機能でカバーできるのは前者だけです。後者、つまり「そのデータの持ち主かどうか」は、自分で1つずつ確認する必要があります。ここが抜けるのが認可漏れです。
やってしまいがちなコード
注文詳細を返すAPIを考えます。
// 危険:URLのIDを信じて、そのまま返している
app.get('/orders/:id', auth, (req, res) => {
const order = db.getOrder(req.params.id);
res.json(order);
});
auth でログインは確認しています。一見よさそうです。
でもこのコード、ログインさえしていれば、他人の注文も見られます。
GET /orders/123 ← 自分の注文
GET /orders/124 ← IDを変えるだけで他人の注文が見える
ユーザーは自分の注文ページを開いた瞬間、URLに注文IDが入っていることに気づきます。あとはその数字を1つずらすだけ。特別なツールもいりません。これが IDOR です。
実際、ECサイトやSaaSの情報漏洩で「連番のIDを変えるだけで他人の個人情報が全部見えた」という事故は、繰り返し起きています。
直し方:所有者チェックを必ず入れる
対策はシンプルです。取得したデータが本当にそのユーザーのものかを、毎回サーバ側で確認します。
app.get('/orders/:id', auth, (req, res) => {
const order = db.getOrder(req.params.id);
// ここが肝:所有者を確認する
if (!order || order.userId !== req.user.id) {
return res.status(404).end();
}
res.json(order);
});
ポイントは2つ。
-
order.userId === req.user.idを必ず突き合わせる。 ログインユーザーのIDはセッション/トークンから取り、URLのIDは信用しない。 - 存在しても 404 を返すのも手。403(権限なし)だと「そのIDは存在する」と教えてしまうため、あえて「無い」と同じ扱いにする考え方です。
他の言語でも考え方は同じです。
# Django:クエリの時点で所有者を条件に入れる
order = get_object_or_404(Order, id=order_id, user=request.user)
# Rails:自分の注文からしか引けないようにする
@order = current_user.orders.find(params[:id])
一覧APIはクエリ段階で絞る
詳細だけでなく、一覧を返すAPIも危険です。
// 危険:全ユーザーの注文を返してしまう可能性
const orders = db.query('SELECT * FROM orders');
一覧は、そもそも自分のデータしか取ってこないようにSQLで絞ります。
// 安全:ログインユーザーの分だけ
const orders = db.query(
'SELECT * FROM orders WHERE user_id = ?',
[req.user.id]
);
「取得してからフィルタする」のではなく「最初から自分の分しか取らない」。これが基本形です。
「URLを隠す」は対策にならない
よくある誤解に、「推測されにくいIDにすれば大丈夫」「管理画面のURLを秘密にすればいい」というものがあります。
これは 隠蔽によるセキュリティ(security by obscurity) と呼ばれ、本質的な対策ではありません。UUIDにするのは"当てにくくする"緩和策としては有効ですが、サーバ側の所有者チェックの代わりにはなりません。URLやIDは、いつか必ず漏れる前提で設計します。
チェックリスト
- データ取得時に「ログインユーザーの持ち物か」を毎回確認している
- URLやリクエストから来たIDやロールを、そのまま信用していない
-
一覧APIはクエリ段階で
WHERE user_id = ?などで絞っている - 管理者向け操作は、URLを隠すのではなくロールをサーバで確認している
- 「IDを1つずらす」を自分で試して、他人のデータが見えないか確認した
最後の項目は特におすすめです。自分のアプリで実際にIDを変えてアクセスしてみる。それだけで認可漏れの多くは発見できます。
もっと体系的に対策したい人へ
この記事で扱った認可漏れは、個人開発で気をつけたい脆弱性の1つにすぎません。パスワードの保存、SQLインジェクション、XSS、CSRF、Cookieの設定、秘密情報の管理……と、知っておくと数行で防げる対策はまだあります。
そうした「個人開発者が今すぐ・最小の労力で入れるべき対策」だけを、実装コードとリリース前チェックリスト付きでまとめた 『個人開発者のためのWebセキュリティ実装ハンドブック』 を用意しました。
まずは**無料のダイジェスト版(全8ページ)**を配布しています。読んでみて合いそうなら、完全版もどうぞ。
- 📗 無料ダイジェスト版(¥0)… https://sekyu-dev.booth.pm/items/8672971
- 📘 完全版(¥980・全45ページ/全21章+8付録+16の事例/コードはJS中心・要所でPHP・Python・Ruby)… https://sekyu-dev.booth.pm/items/8634404
本記事・書籍は一般的なベストプラクティスをまとめた教育目的の内容です。扱う情報の重要度に応じて、OWASP等の一次情報も併せて参照してください。