2-6. 認可 - RBAC / ABAC / ReBAC・IDOR・マルチテナント
所要時間: 40-60分(がっつりなら2セッション分) ゴール: 認証と認可を区別し、RBAC / ABAC / ReBAC の使い分けと IDOR 防止、マルチテナント認可を Go で実装できる コミット内容:
~/learn/security/day-authz/にロールベース認可ミドルウェアとリソース所有者チェックの実装
前章とのつながり
このレッスンの位置づけ
1-1 認証と認可 で「概念」を学んだ。本章は その実装パターン。Level 2 の最後を飾る、認可の 深掘り 回。
- 1-1 で予告した RBAC / ABAC の 具体実装 をここでやる
- ReBAC(関係ベース、Google Zanzibar / OpenFGA)も追加
- IDOR という名前で再登場する「リソース所有者チェック」は実務で最頻出
- 2-5 入力検証 と連動: ID 入力を信じず必ず認可チェック
次は Level 3 3-1 サーバーhardening へ。アプリ層の守りから インフラ層の守り へ視点が移る。
実害シーン: 認可不備の被害
- First American Financial (2019): URL の数字を1つ変えるだけで他人の住宅ローン書類 → 8.85億件流出
- Optus (2022): ID をインクリメントするだけで全顧客取得 → 970万件流出、CEO 辞任
- Peloton (2021): 認可なし内部 API、任意他人の個人情報取得可能
- Snapchat (2014): 認可不足で4.6万件の電話番号流出
OWASP API Security Top 10 で BOLA (IDOR系) が長年 #1。「認証は通ったから何でも見せる」を絶対に許さない。
大前提: 認証 (Authentication) と認可 (Authorization) の違い
| 用語 | 略 | 答える質問 | 例 |
|---|---|---|---|
| 認証 (Authentication) | AuthN | あなたは誰? | パスワード照合、JWT 検証 |
| 認可 (Authorization) | AuthZ | あなたは何をしていい? | この記事を編集できる? |
認証は「玄関の鍵」、認可は「家の中の部屋ごとの鍵」。両方必要。
セキュリティ事故の事例は認証よりも認可不備の方が多い。なぜなら認証は「ライブラリで型がある程度決まっている」のに対し、認可は「ビジネスロジックそのもの」だから自前実装になり、見落としが出やすい。
実例:
- 2014年 Snapchat の API: 認可チェック不足で電話番号と相手 ID の照合が誰でもできた → 4.6万件の電話番号 + 名前流出
- 2019年 First American Financial: 単純な URL の番号書き換え(IDOR)で 8.85億件 の権利保険文書が見れた状態
- 2021年 Peloton: 認可なしの内部 API、Userエンドポイントから任意の他人の個人情報取得可能
- 2024年 Optus (Australia): ID をインクリメントするだけで全顧客データ取得、970万人分流出
「認証は通しているが認可が抜けている」が攻撃成功率の最も高いパターン。
セッション①: 認可モデル(30分)
1. RBAC (Role-Based Access Control)
最もメジャーな認可モデル。ロール(役割)に権限を結びつけ、ユーザーにロールを割り当てる。
User → has → Role → has → Permission
データモデル例:
CREATE TABLE users (id INT PRIMARY KEY, email TEXT);
CREATE TABLE roles (id INT PRIMARY KEY, name TEXT); -- "admin", "editor", "viewer"
CREATE TABLE permissions (id INT PRIMARY KEY, name TEXT); -- "article:write", "user:delete"
CREATE TABLE user_roles (user_id INT, role_id INT);
CREATE TABLE role_permissions (role_id INT, permission_id INT);RBAC が広く使われる理由
- シンプル: 概念が3つ(user, role, permission)だけで設計しやすい
- 管理画面で扱いやすい: 「このユーザーを editor にする」だけで権限が決まる
- 監査しやすい: 「誰が何をできるか」がロール経由で説明可能
Kubernetes RBAC、AWS IAM、GitHub の権限などすべて RBAC の発展形。
RBAC の限界(ロール爆発)
細かい権限を扱おうとすると「営業部の山田だけ顧客Aを編集可能」のようなケースで:
role: sales-yamada-customer-A-editorのような細粒度ロールを大量に作ることになる- ロール数が10倍になると管理不能 → ロール爆発
解決策: 後述の ABAC / ReBAC、または「リソース所有者チェック」と組み合わせる。
2. ABAC (Attribute-Based Access Control)
ユーザー・リソース・環境の属性(attribute)を見て、ポリシーで判定する方式。
ポリシー例:
allow if
user.department == resource.department
and user.clearance >= resource.classification
and request.time within working_hours
ABAC が向くケース
- 属性が動的・多次元: 部署、地域、時間帯、デバイス、信頼スコアなど
- コンテキストで判定したい: 「社内ネットワークからのみ」「営業時間内のみ」
- 規制業界: 医療、金融、政府系
実装手段:
- OPA (Open Policy Agent): Rego という DSL で書く、CNCF プロジェクト
- AWS IAM のポリシー: Condition で属性ベース判定
- Cedar (AWS): ABAC 用ポリシー言語
# OPA Rego の例
package authz
default allow = false
allow {
input.user.role == "admin"
}
allow {
input.action == "read"
input.user.department == input.resource.department
}3. ReBAC (Relationship-Based Access Control)
「ユーザーとリソースの関係」をグラフで持ち、関係性から判定する方式。
例:
- ユーザー A が 「ドキュメント X の owner」
- ユーザー A が ユーザー B を 「ドキュメント X の editor として招待」
- B は親フォルダ Y の 「viewer」
→ 「B はドキュメント X を編集できるか?」のような問いに、関係グラフを辿って答える。
ReBAC の原点: Google Zanzibar
- 2019年に Google が論文発表した、Google Drive / YouTube / Photos などで使われている内部認可システム
- 1秒あたり数千万クエリを処理、低レイテンシ
- 「user X is editor of doc Y」のような タプルで関係を表現
派生 OSS:
- SpiceDB (Authzed): Zanzibar 直系のオープンソース
- OpenFGA (Auth0/Okta): Zanzibar インスパイア
- Permify, Warrant: 商用クローン
Google Docs / Drive のような「ファイルを誰かと共有」「フォルダの権限を継承」型のサービスでは ReBAC がはまる。
4. 認可ロジックを「どこに置くか」
[クライアント]
↓
[API Gateway / WAF] ← 大雑把な許可(IP, レート制限)
↓
[ハンドラ / ミドルウェア] ← ロール判定(admin only など)
↓
[サービス層] ← ビジネスルール(このユーザーがこの操作をできるか)
↓
[DB アクセス層] ← リソース所有者チェック
↓
[DB] ← Row Level Security(最終の砦)
多層で認可する理由
どこか1層に穴があっても、他の層で止まる。深層防御 (Defense in Depth)。
- ミドルウェアだけ: ハンドラ実装時にミドルウェアの適用を忘れた瞬間に穴
- サービス層だけ: ミドルウェアの軽い判定が無いと、DoS や認証ナシのアクセスが奥まで届く
- DB Row Level Security だけ: SQL を書く全箇所を信頼しないといけない(マイグレーションスクリプトなどから漏れる)
3層全部やるのが理想。最低でも「ミドルウェア + サービス層」。
5. Postgres Row Level Security (RLS)
「アプリのコードが何をしようと、DB が許さない」 最強の砦:
ALTER TABLE articles ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON articles
USING (tenant_id = current_setting('app.current_tenant')::int);アプリは接続後に SET app.current_tenant = 42 を実行 → 以後 articles の SELECT/UPDATE は tenant_id=42 のみ可視。SQL を書き間違えても他テナントのデータが返らない。
RLS が活きるケース
- マルチテナント SaaS: テナント間データ漏洩は最大の事故、RLS で保険
- マルチユーザー個人データ: ユーザーIDで自動フィルタ
Supabase は RLS をフロント直アクセスの安全基盤として使っている。
セッション②: 実装と典型脆弱性(30分)
6. Go: ロール判定ミドルウェア
package main
import (
"context"
"net/http"
)
type contextKey string
const userKey contextKey = "user"
type User struct {
ID string
Role string // "admin", "editor", "viewer"
}
// 認証ミドルウェア(JWT 検証してユーザーをコンテキストに入れる)
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, err := verifyTokenAndLoadUser(r) // [[2-1_JWT|JWT レッスン]] 参照
if err != nil {
http.Error(w, "unauthorized", 401)
return
}
ctx := context.WithValue(r.Context(), userKey, u)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// 認可ミドルウェア: 必要なロールを要求
func requireRole(role string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, ok := r.Context().Value(userKey).(*User)
if !ok {
http.Error(w, "unauthenticated", 401)
return
}
if u.Role != role {
http.Error(w, "forbidden", 403)
return
}
next.ServeHTTP(w, r)
})
}
}
func main() {
mux := http.NewServeMux()
mux.Handle("/admin/users", authMiddleware(requireRole("admin")(adminHandler())))
http.ListenAndServe(":8080", mux)
}このコードの設計ポイント
- 401 (unauthorized): 認証なし/JWT 失敗
- 403 (forbidden): 認証は通ったがロール不足
- HTTP ステータスを正しく使い分ける(運用ログで切り分けやすい)
7. リソース所有者チェック(IDOR 防止)
ロールだけだと「editor は他人の記事も編集できる?」が解決しない。「このユーザーがこのリソースの所有者か」を毎回確認する:
func updateArticle(w http.ResponseWriter, r *http.Request) {
u := r.Context().Value(userKey).(*User)
articleID := r.PathValue("id")
// 1. 認可: 自分の記事のみ編集可
article, err := db.GetArticle(articleID)
if err != nil {
http.Error(w, "not found", 404)
return
}
if article.OwnerID != u.ID && u.Role != "admin" {
http.Error(w, "forbidden", 403)
return
}
// 2. 本処理
// ...
}「常に WHERE owner_id = $user_id を入れる」癖
// 良いパターン: クエリ自体に所有者条件を入れる err := db.QueryRow( "SELECT * FROM articles WHERE id = $1 AND owner_id = $2", articleID, u.ID, ).Scan(...) if err == sql.ErrNoRows { // 「存在しない」「他人のもの」を区別せず 404 http.Error(w, "not found", 404) }「2回クエリ叩いて1回目で所有者確認」より、最初のクエリに条件を埋め込むほうが堅い(条件追加し忘れがない)。
エラー時の応答は
403 Forbiddenよりも404 Not Foundの方が情報漏洩を抑える設計もある(存在自体を隠す)。
8. IDOR (Insecure Direct Object Reference) 実例
GET /api/orders/12345
→ 認証は通っているが、order 12345 がこのユーザーのものか確認していない
→ 攻撃者は order_id を 12345, 12346, 12347 と変えるだけで他人の注文を全部見れる
IDOR は事故が多すぎる
実例:
- 2019 First American Financial: URL の ID をインクリメントするだけで 8.85億件の文書アクセス可能
- 2021 Peloton: ユーザー ID で /api/user/
を叩けば誰でも見える - 2024 Optus: 970万人の顧客データ、ID 番号差し替えで取得可能だった
共通点: 認証は通している、URLの ID で直接リソースを指している、所有者チェックが無い。
防止策:
- すべての ID パラメータで所有者チェック (
WHERE owner_id = $user_id)- 推測困難な ID (UUID v4) を使う(補助的、根本対策ではない)
- テストで網羅: 別ユーザーで他人の ID を叩いて 403/404 が返ることを CI で確認
アンチパターン: ID をフロントから受け取って素で使う
// NG: ID 渡されたらそのまま使う func getOrder(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("id") order, _ := db.GetOrder(id) // 所有者チェックなし json.NewEncoder(w).Encode(order) }攻撃者がログイン後、
?id=1,?id=2, … と総当たりで全顧客データ取得。
9. マルチテナント認可(最大級の事故源)
SaaS では「テナント(会社)A の社員がテナント B のデータを見える」事故が致命的。
設計の原則:
- すべてのテーブルに
tenant_id(またはorg_id,workspace_id) - クエリは必ず
WHERE tenant_id = ? - 認証時にユーザーのテナントID をコンテキストに入れる
- Postgres Row Level Security で DB 層に保険を入れる
- テストで「別テナントから見えない」を必ず検証
// テナント認可ミドルウェア
func tenantMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u := r.Context().Value(userKey).(*User)
// すべてのクエリで tenant_id を強制したいので、DB 接続単位で SET
_, _ = db.ExecContext(r.Context(),
"SET LOCAL app.current_tenant = $1", u.TenantID)
next.ServeHTTP(w, r)
})
}マルチテナント漏洩の被害
B2B SaaS で「うちのデータが他社に見えた」が起きると:
- 全顧客に通知 → 信頼失墜、契約解除
- 法務問題(NDA 違反、GDPR)
- 株価下落、買収機会喪失
一度起きると会社が傾く。最も慎重に設計するべき領域。
マルチテナント設計の3パターン
- シングル DB / shared schema:
tenant_id列で論理分離(最も一般的、コスト効率良い)- シングル DB / 別 schema: テナントごとに Postgres schema 分離(移行や bulk 操作が楽)
- 別 DB: 完全物理分離(最も安全、運用コスト最大)
多くの SaaS は1で start、規制要件があれば2-3にハイブリッド。
10. アンチパターン総まとめ
フロントだけで認可
// NG: 「admin 用のメニューを非表示」だけ if (user.role === 'admin') showAdminMenu();攻撃者は curl で API を直叩き。「管理API URL を知っているか」だけで叩ける状態に。
必ず API 側でロール判定。
認可なしの内部 API
「これは社内向け API だから」と認可を入れない → IP 制限が破られた瞬間に全公開。社内 API でも認可必須(多層防御)。
IDOR を放置
既述の通り。すべての ID で所有者チェック。
ロール文字列の比較ミス
if u.Role == "Admin" { ... } // NG: 大文字小文字でハマる列挙型を定義して文字列比較を避ける:
type Role string const ( RoleAdmin Role = "admin" RoleEditor Role = "editor" )
11. デフォルト拒否の原則
設計指針: Deny by Default
- デフォルトの状態が「拒否」
- 明示的に allow したものだけ通る
良い例:
switch u.Role { case "admin": // 許可 case "editor": if isOwner { /* 許可 */ } else { http.Error(w, "forbidden", 403); return } default: http.Error(w, "forbidden", 403) // 不明なロールは拒否 }悪い例:
if u.Role == "guest" { http.Error(w, "forbidden", 403) return } // 上で弾かれなければ通る ← 新しいロール追加時に事故る「allow リストにあるか?」で判定する。「deny リストにあるか?」で判定すると、新しいロールが増えた時に漏れる**。
12. テストで認可を検証
func TestArticleAuthZ(t *testing.T) {
cases := []struct {
name string
actor *User
articleOwn string
wantStatus int
}{
{"owner can edit", &User{ID: "u1"}, "u1", 200},
{"other can't edit", &User{ID: "u2"}, "u1", 403},
{"admin can edit", &User{ID: "u3", Role: "admin"}, "u1", 200},
{"unauthenticated", nil, "u1", 401},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
// ... リクエスト発火、ステータス確認
})
}
}認可テストは「正例」より「負例」が大事
- 正例(自分のリソースを操作できる)はバグでも気づきやすい
- 負例(他人のリソースを操作できない)はバグでも一見動くので気づきにくい
- 「別ユーザーになりすまして他人のリソースを叩く」テストを必ず書く
- CI で実行して「IDOR が再発しないか」継続的に保証
練習課題
mkdir -p ~/learn/security/day-authz
cd ~/learn/security/day-authz- RBAC の最小実装:
users / roles / permissionsの3テーブルを Postgres に作り、Go で「特定の permission を持つか」をチェックする関数を書く requireRole("admin")ミドルウェアを実装し、admin ロールでない user が 403 を受けることを確認- 記事編集 API を実装し、別ユーザーが他人の記事を編集しようとすると 403(または 404)を返すことを確認
- マルチテナント:
articlesテーブルにtenant_idを追加、テナント A のユーザーがテナント B の記事を見れないことを確認 - Postgres の Row Level Security を有効化し、SQL レベルでもクロステナントアクセスが防がれることを確認
- 認可テストを4ケース以上書き、CI で実行
締め: git で証跡を残す
cd ~/learn/security/day-authz
git add .
git commit -m "feat(security): RBAC + リソース所有者チェック + マルチテナント認可"チェックリスト
- 認証 (AuthN) と認可 (AuthZ) を1分で区別して説明できる
- RBAC / ABAC / ReBAC の使い分けを言える
- Google Zanzibar 系(SpiceDB, OpenFGA)の存在を知っている
- 認可ロジックを「複数層」に置く理由を語れる
- Postgres Row Level Security でテナント分離を書ける
- IDOR の事例(First American, Optus 等)を知り、防止策を実装できる
- 「すべてのクエリに owner_id / tenant_id を入れる」癖がある
- 認可テストで「別ユーザーで他人のリソースを叩く」ケースを書く習慣がある
- Deny by Default の意味と実装パターンを語れる
詰まった時のチートシート
| やりたいこと | コード / SQL |
|---|---|
| ロール判定ミドルウェア | requireRole("admin") で 403 |
| 所有者チェック | WHERE id=? AND owner_id=? をクエリに含める |
| テナント分離 | WHERE tenant_id=? + RLS |
| Postgres RLS 有効化 | ALTER TABLE ... ENABLE ROW LEVEL SECURITY |
| ポリシー定義 | CREATE POLICY name ON table USING (条件) |
| OPA Rego | allow { input.user.role == "admin" } |
| 認可テスト | 別ユーザーで叩いて 403/404 確認 |
| ステータスコード | 401: 認証なし / 403: 認可なし / 404: 存在隠蔽 |
「実務OK」基準
- 新規 API を書く時、「認可は誰がチェックする?」を必ず最初に考える
- コードレビューで「これ ID 直で受けて owner チェックしていない」を即座に指摘できる
- マルチテナント SaaS の設計で
tenant_idを全テーブルに入れる + RLS を提案できる - 認可テストを書く文化を持ち、CI で IDOR を継続的に検出できる
- RBAC で足りなくなった時に ABAC / ReBAC への移行判断ができる
さらに深掘りするなら
- Google Zanzibar 論文 「Zanzibar: Google’s Consistent, Global Authorization System」(2019)
- SpiceDB / OpenFGA のドキュメント - ReBAC OSS の実装
- OPA (Open Policy Agent) チュートリアル - ABAC の現代標準
- AWS IAM ポリシー言語の公式ドキュメント - ABAC の実用例
- OWASP API Security Top 10 - BOLA (IDOR 系) が長年 #1
- Postgres 公式ドキュメント Row Security Policies
- First American Financial 事件の Krebs on Security 記事 - 「8.85億件 IDOR」事件の詳細
メタ認知: 自分の理解度を診断する
30秒セルフテスト
- RBAC / ABAC / ReBAC をそれぞれ 1文 で
- IDOR を防ぐ「リソース所有者チェック」の Go 実装パターン
- マルチテナントで
tenant_idを どこで 強制するか(ヒント: 全クエリ・全エンドポイント)- Postgres の Row Level Security の使い所
- 認可テストを CI で書く価値(IDOR を継続検出)
詰まったら → セッション①の認可モデル比較、IDOR 防止パターン、マルチテナント章を再読。
Level 2 完走の振り返り(メタ認知)
Level 2 で身につけたこと
- JWT を正しく実装し、脆弱性を避ける (2-1)
- セッション/Cookie を実務判断で設定 (2-2)
- CSRF と SSRF を区別して防ぐ (2-3)
- シークレットを開発・CI・本番の各段階で守る (2-4)
- 入力を allow-list で検証 (2-5)
- 認可を RBAC/ABAC/ReBAC で設計、IDOR を防ぐ (2-6)
Level 1 の概念が コードと運用に落とし込まれた フェーズが完了。次は Level 3 で 「運用フェーズ」 の守り(サーバーhardening / 依存スキャン / ログ監視 / インシデント対応 / 脅威モデリング)へ。
次のレッスン
3-1 サーバーhardening へ。Level 3 は インフラ層の守り と 運用プロセス にフォーカス。アプリの守りを固めても、サーバー側に穴があれば全部台無し。
間隔反復ポイント
認可は 「ビジネスロジックそのもの」 なので、新機能追加のたびに認可を考える必要がある。1ヶ月後にこのページの「マルチテナント認可」と「IDOR 防止」を眺める習慣で、新機能設計時に反射的に思い出せる。