そのAPI、認可チェックしてますか?
自社サービスのマイページ機能を実装した時の話です。プロフィール情報を取得するAPIのエンドポイントはこうなっていました。
GET /api/users/1052/profile
ある日、テスト中にふと思い立ち、URLの 1052 を 1053 に変えてブラウザのアドレスバーに叩き込んでみました。
表示されたのは、自分ではない見知らぬユーザーの氏名、メールアドレス、電話番号。
認証(ログインしていること)はチェックしていましたが、認可(そのデータにアクセスする権限があるか)は一切チェックしていなかったのです。
これが IDOR(Insecure Direct Object Reference:安全でない直接オブジェクト参照) と呼ばれる脆弱性です。OWASP Top 10の「Broken Access Control(アクセス制御の不備)」に分類され、バグバウンティ(脆弱性報奨金プログラム)で最も頻繁に報告される脆弱性の一つでもあります。
なぜ見落とされるのか
IDORの厄介さは、コードとしては正常に動いている点にあります。
リクエストを受け取り、DBからデータを引き、レスポンスを返す。ロジックにバグはありません。ただ「その人がそのデータを見ていい人か?」の1行が抜けているだけです。
# 脆弱なコード
@app.get("/api/users/{user_id}/profile")
def get_profile(user_id: int, current_user = Depends(get_current_user)):
return db.query(User).filter(User.id == user_id).first()
# ↑ current_user と user_id の一致をチェックしていない!
# 安全なコード
@app.get("/api/users/{user_id}/profile")
def get_profile(user_id: int, current_user = Depends(get_current_user)):
if current_user.id != user_id:
raise HTTPException(status_code=403, detail="Forbidden")
return db.query(User).filter(User.id == user_id).first()
防御策
1. 全てのAPIエンドポイントで「認可チェック」を義務化する
認証(Authentication)だけでなく、認可(Authorization)が漏れていないかをコードレビューのチェックリストに入れましょう。
2. IDを連番にしない(UUIDの採用)
/users/1052 のような連番IDは、攻撃者が「+1して次のユーザーを見る」という予測を容易にします。UUIDv4(550e8400-e29b-41d4-a716-446655440000)を使えば推測が困難になります。ただし、UUIDにしたからといって認可チェックを省略してはいけません。あくまで「推測困難にする」だけであり、根本的な対策ではありません。
3. 自動テストで認可漏れを検出する
「ユーザーAのトークンで、ユーザーBのリソースにアクセスしたら403が返ること」というテストケースを、全エンドポイントに対して書きましょう。