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 で直接リソースを指している、所有者チェックが無い

防止策:

  1. すべての ID パラメータで所有者チェックWHERE owner_id = $user_id
  2. 推測困難な ID (UUID v4) を使う(補助的、根本対策ではない)
  3. テストで網羅: 別ユーザーで他人の 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パターン

  1. シングル DB / shared schema: tenant_id 列で論理分離(最も一般的、コスト効率良い)
  2. シングル DB / 別 schema: テナントごとに Postgres schema 分離(移行や bulk 操作が楽)
  3. 別 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
  1. RBAC の最小実装: users / roles / permissions の3テーブルを Postgres に作り、Go で「特定の permission を持つか」をチェックする関数を書く
  2. requireRole("admin") ミドルウェアを実装し、admin ロールでない user が 403 を受けることを確認
  3. 記事編集 API を実装し、別ユーザーが他人の記事を編集しようとすると 403(または 404)を返すことを確認
  4. マルチテナント: articles テーブルに tenant_id を追加、テナント A のユーザーがテナント B の記事を見れないことを確認
  5. Postgres の Row Level Security を有効化し、SQL レベルでもクロステナントアクセスが防がれることを確認
  6. 認可テストを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 Regoallow { 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秒セルフテスト

  1. RBAC / ABAC / ReBAC をそれぞれ 1文
  2. IDOR を防ぐ「リソース所有者チェック」の Go 実装パターン
  3. マルチテナントで tenant_idどこで 強制するか(ヒント: 全クエリ・全エンドポイント)
  4. Postgres の Row Level Security の使い所
  5. 認可テストを CI で書く価値(IDOR を継続検出)

詰まったら → セッション①の認可モデル比較、IDOR 防止パターン、マルチテナント章を再読。

Level 2 完走の振り返り(メタ認知)

Level 2 で身につけたこと

  1. JWT を正しく実装し、脆弱性を避ける (2-1)
  2. セッション/Cookie を実務判断で設定 (2-2)
  3. CSRF と SSRF を区別して防ぐ (2-3)
  4. シークレットを開発・CI・本番の各段階で守る (2-4)
  5. 入力を allow-list で検証 (2-5)
  6. 認可を RBAC/ABAC/ReBAC で設計、IDOR を防ぐ (2-6)

Level 1 の概念が コードと運用に落とし込まれた フェーズが完了。次は Level 3 で 「運用フェーズ」 の守り(サーバーhardening / 依存スキャン / ログ監視 / インシデント対応 / 脅威モデリング)へ。

次のレッスン

3-1 サーバーhardening へ。Level 3 は インフラ層の守り運用プロセス にフォーカス。アプリの守りを固めても、サーバー側に穴があれば全部台無し。

間隔反復ポイント

認可は 「ビジネスロジックそのもの」 なので、新機能追加のたびに認可を考える必要がある。1ヶ月後にこのページの「マルチテナント認可」と「IDOR 防止」を眺める習慣で、新機能設計時に反射的に思い出せる。