現場のエンジニア諸君、お疲れ様。今日もどこかのログで不穏な挙動を検知してヒヤリとしているかもしれないが、セキュリティの現場とはそういうものだ。
今日は、最近の認可設計で「便利さ」の裏に隠れた「時限爆弾」になりつつある、OAuth 2.0のインクリメンタル認可(Incremental Authorization)について話そう。
「ユーザーにいきなり全部の権限を求めると離脱されるから、必要な時に必要な分だけスコープを追加しよう」──このUX向上策、設計を誤ると「権限の累積(Permission Creep)」という泥沼に足を踏み入れることになる。
—
1. なぜ「インクリメンタル認可」が脆弱性になるのか
インクリメンタル認可とは、例えば「まずはメールアドレスだけ」→「次はカレンダーへのアクセス」→「最後は支払い情報」と、段階的にスコープを広げていく仕組みだ。
ここで開発者が陥りやすい最大の盲点は、「一度許可されたスコープは、ユーザーが明示的に取り消さない限り、裏側で永久に累積し続ける」という点にある。
攻撃シナリオ:スコープの「積み増し」による権限昇格
攻撃者が正規のユーザーを装い、あるいはフィッシングサイトへ誘導して、「正当なプロセス」として徐々にスコープを要求していく。被害者は「機能追加かな?」と思って同意ボタンを押すが、気づいた時にはアプリが持っているアクセストークンが、本来不要なはずの強力な権限(adminやwrite:allなど)を保持している状態になる。
もし、このアプリがクロスサイトスクリプティング(XSS)等の脆弱性を抱えていたらどうなる? 攻撃者は、ユーザーが一生懸命「インクリメンタルに許可」してくれた強力なトークンを奪い、本来そのアプリが持つべきでない権限でバックエンドを蹂躙する。これが「権限の累積リスク」の正体だ。
—
2. 防御の要:スコープの「強制整理」とRevocationの実装
防御の鉄則は「最小権限の原則(Least Privilege)」を認可のライフサイクル全体に適用することだ。以下の2点を徹底しろ。
1. スコープの「リセット」設計: 特定の機能を使わなくなった場合、あるいはセッションが切れた場合は、古いトークンを破棄し、再認可時に必要なスコープだけを再要求する。
2. 認可取り消し(Revocation)の自動化: RFC 7009 に準拠したRevocationエンドポイントを叩く実装を必ず入れろ。
—
3. 実践:Python (Flask/Authlib) によるRevocation実装
ただトークンを捨てるのではなく、認可サーバー側でしっかりと無効化させる必要がある。以下は、ログアウト時や機能停止時にトークンを確実に無効化する処理のサンプルだ。
import requests
from oauthlib.oauth2 import BackendApplicationClient
def revoke_access_token(token, token_type_hint=’access_token’):
“””
OAuth 2.0 Revocation Endpoint (RFC 7009) を呼び出し、
認可サーバー側で権限を強制無効化する。
“””
revocation_url = “https://your-auth-server.com/oauth/revoke”
# クライアント認証が必要な場合はclient_id/secretを付与
payload = {
‘token’: token,
‘token_type_hint’: token_type_hint,
‘client_id’: ‘YOUR_CLIENT_ID’,
‘client_secret’: ‘YOUR_CLIENT_SECRET’
}
try:
response = requests.post(revocation_url, data=payload)
response.raise_for_status()
print(“トークンの無効化に成功しました。”)
except requests.exceptions.RequestException as e:
# ここで失敗を放置すると「ゾンビトークン」が残る。必ずエラーログを吐け。
print(f”トークンの無効化に失敗: {e}”)
使用例: ユーザーが「データ同期機能」をオフにした時
revoke_access_token(current_user.access_token)
—
4. インフラ側で防ぐ:WAFによるスコープの監視(Nginx/Lua)
アプリケーションコードの修正は必須だが、インフラ側で「異常に広いスコープを持つトークン」を拒否する防波堤を作ることも有効だ。
NginxでJWTを検証し、スコープの値をチェックするLuaスクリプトの断片を載せておく。
— Nginx + OpenResty でのスコープ検証イメージ
local jwt = require “resty.jwt”
local jwt_token = ngx.req.get_headers()[“Authorization”]:sub(8)
local jwt_obj = jwt:verify(“SECRET_KEY”, jwt_token)
— 「危険なスコープ」が含まれていたら即座に遮断する
local forbidden_scopes = {“admin”, “super-user”, “write:all”}
for _, scope in ipairs(jwt_obj.payload.scope) do
for _, forbidden in ipairs(forbidden_scopes) do
if scope == forbidden then
ngx.log(ngx.ERR, “不正な権限の持ち込みを検知: ” .. scope)
ngx.exit(ngx.HTTP_FORBIDDEN)
end
end
end
—
現場のリーダーから諸君へ
「とりあえず便利だから」という動機で実装されたインクリメンタル認可は、時として開発者自身の手でバックドアを開けているのと同じだ。
- 定期的な棚卸し: ユーザーが過去に許可したスコープを一覧表示し、「今は使っていない権限」をユーザー自身に取り消させるUIを作れ。
- 認可のスコープ制限: 認可リクエストを送る際、必ず必要最小限のスコープだけに絞ったリクエストURLを生成せよ。
セキュリティとは、「システムに何ができるか」ではなく「システムに何をさせないか」を定義する作業だ。面倒な実装こそが、君たちのプロダクトを、そしてユーザーのデータを守る唯一の防壁になる。
コードを書く前に、一度立ち止まって考えてみてほしい。「この権限、本当に今すぐ必要か?」と。それができるエンジニアこそが、真のプロフェッショナルだ。
コメント