# AIが作ったログイン機能は本物か — 「ログインしてる風」と本物の権限チェックの違い

> ログイン画面があるのに他人のデータが見えてしまう——AI生成アプリで最も多い脆弱性を、非エンジニア向けに解説します。URLの番号を書き換えるだけで確認できる手順、OWASPがAPIリスク第1位に挙げる理由、そしてAIに貼れる修正指示まで。

- 公開日: 2026-08-06
- 著者: 友田 陽大
- タグ: バイブコーディング, セキュリティ, 認証・認可, 個人開発, Next.js
- URL: https://tomodahinata.com/blog/ai-generated-login-vs-real-authorization
- カテゴリ: AIで作ったアプリの運営
- 総合ガイド: https://tomodahinata.com/blog/ai-app-pre-launch-guide-for-non-engineers

## 要点

- 「ログイン画面がある」ことと「他人のデータが守られている」ことは別問題。前者は本人確認（認証）、後者は権限確認（認可）で、AIが書き漏らすのは後者
- URLの番号を1つ変えるだけで他人のデータが見える状態は、OWASPがAPIリスクの第1位に挙げる BOLA（オブジェクトレベル認可の不備）そのもの
- 一覧画面は正しく絞られるのに詳細画面が素通しになる、という非対称がAI生成コードで頻出。一覧が正しいので動作確認では絶対に気づかない
- 確認はブラウザだけでできる。テスト用アカウントを2つ作り、片方のURLをもう片方で開くだけ
- フロントエンドで画面を隠すのは防御ではない。判定はサーバー側で行い、RLSと合わせて二重にするのが正しい状態

---

ログイン画面があります。パスワードも要求します。ログインしないと何も表示されません。

それでも、**URLの数字を1つ書き換えるだけで他人のデータが見える**——AI で作られたアプリで最も多い脆弱性です。

しかもこれは、あなたが自分のアカウントで使っている限り、絶対に気づけません。完璧に正常に動くからです。

---

## 1. 認証と認可 — 2つの違う仕事

まず言葉を整理します。この2つが混ざっているのが、問題の根っこです。

**認証（Authentication）= あなたが誰かを確かめる**
ログイン画面のことです。メールアドレスとパスワードで「確かにこの人だ」と判断します。

**認可（Authorization）= あなたがそれをしてよいかを確かめる**
ログイン済みのAさんが、注文番号124（Bさんの注文）を見てよいか。これは別の判断です。

例えるなら、認証は**社員証を見せて入館すること**、認可は**入った建物の中で、どの部屋に入れるか**です。入館できたからといって、社長室に入れるわけではありません。

AI が書き漏らすのは、ほぼ常に **認可** のほうです。

理由はシンプルで、認証は「ログイン機能を作って」と頼めば作られる、目に見える機能だからです。一方の認可は、画面上に何も現れません。**正しく実装されていても、されていなくても、見た目は同じ**です。

---

## 2. 何が起きるのか — 具体例

あなたのアプリに、注文詳細の画面があるとします。

```
https://あなたのアプリ.com/orders/123
```

`123` はあなたの注文番号です。ここで `124` に書き換えます。

```
https://あなたのアプリ.com/orders/124
```

**もし他人の注文が表示されたら、それが脆弱性です。**

OWASP（Webセキュリティの標準的な団体）は、これを **API1:2023 Broken Object Level Authorization（BOLA、オブジェクトレベル認可の不備）** として、**APIリスクの第1位** に挙げています。1位です。最も多く、最も影響が大きい。

### なぜ「見えないリンク」では守れないのか

「画面に他人の注文へのリンクは出していないから大丈夫」——これが最も多い誤解です。

リンクが出ていなくても、**URLを直接入力すればリクエストは飛びます**。ブラウザの開発者ツールを開けば、アプリがどんな形の通信をしているかも分かるので、URLを組み立てることは誰にでもできます。

「画面に出ていない＝アクセスできない」は成立しません。UIは飾りであって、鍵ではありません。

---

## 3. なぜAIが書き漏らすのか — 一覧と詳細の非対称

ここが面白いところで、AI は**一覧画面はきちんと絞り込みます**。

「ログイン中のユーザーの注文一覧を表示して」と頼まれれば、AI は「ログイン中のユーザーの」という条件を確かに使います。だから一覧には自分の注文しか出ません。

ところが「注文詳細を表示して」と頼まれると、**番号で引いてくるだけ**のコードを書きがちです。

```
一覧:  「ログイン中の人の注文を全部持ってきて」  → 正しく絞られる
詳細:  「注文番号 123 を持ってきて」              → 誰のものか確認していない
```

詳細画面には「ログイン中のユーザーの」という条件が付いていない。頼まれていないからです。

そして——**一覧が正しく絞られているので、動作確認では絶対に気づきません**。自分のアカウントで一覧を開き、そこから詳細に飛ぶ限り、常に自分のデータしか出てこないからです。

この非対称が、AI 生成アプリでこの脆弱性が量産される理由です。

### もっと危険な変種

同じ問題は、読み取りだけでなく**更新や削除**でも起きます。

- 他人の投稿を削除できる
- 他人のプロフィールを書き換えられる
- 他人の注文をキャンセルできる

読み取りより実害が大きいのに、確認されることは少ない箇所です。

---

## 4. 自分で確認する手順 — ブラウザだけでできる

コードを一切読まずに確認できます。公開前に必ず1回やってください。

### 手順

1. **テスト用アカウントを2つ作る**（Aさん・Bさん）
2. **Aさんでログイン**して、何かデータを作る（投稿、注文、メモ、なんでも）
3. そのデータの詳細画面を開き、**URLを控える**。番号やIDが入っているはずです
4. **ログアウトして、Bさんでログイン**する
5. 控えたURLを**アドレスバーに直接入力**する

### 判定

- **Aさんのデータが表示された** → 脆弱性です。この記事の修正指示へ
- **エラー、404、「権限がありません」になった** → 正常です

### 応用: 更新・削除も試す

読み取りが守られていても、更新が守られているとは限りません。Bさんでログインした状態で、Aさんのデータの編集画面（`/orders/123/edit` のようなURL）も試してください。

さらに踏み込むなら、**ログアウトした状態**で同じURLを開いてみてください。ログイン画面にリダイレクトされれば正常です。データが見えたら、認証すら効いていません。

---

## 5. 何が正しい実装なのか

難しい話ではありません。**データを取ってくるときに、必ず「ログイン中の人のものか」を条件に含める**。それだけです。

```
【危険】番号だけで引く
  「注文番号 123 を持ってきて」

【正しい】番号 + 所有者で引く
  「注文番号 123 で、かつ 所有者が今ログインしている人のもの を持ってきて」
```

正しい形では、他人の注文番号を指定しても**該当なし**になります。「見つからない」と「権限がない」が同じ結果になるので、番号の存在自体も推測されません。

### 大事な原則: 「隠す」は防御ではない

フロントエンド側でボタンを非表示にする、メニューから項目を消す——これらは**使いやすさの工夫であって、防御ではありません**。

判定は必ずサーバー側で行います。ブラウザ側のコードは、利用者が自由に書き換えられるからです。

### RLS との関係 — 二重にする

[RLS](/blog/supabase-rls-explained-for-non-engineers)を正しく設定していれば、データベース層でも同じ防御がかかります。では、サーバー側のチェックは不要でしょうか。

**不要にはなりません。** 守る範囲が違うからです。

- **RLS** = データベース層。どの経路から来ても効く
- **サーバー側チェック** = アプリケーション層。業務ルールを表現できる

たとえば `service_role` キーを使う管理処理は RLS を素通りします。そこではサーバー側チェックだけが防御になります。逆に、AIが新しいAPIを追加してチェックを書き忘れても、RLS があれば止まります。

**両方あるのが正しい状態**です。片方が抜けたときにもう片方が受け止める、という設計です。

---

## 6. 管理者機能 — もう一段の落とし穴

管理者向けの画面（全ユーザー一覧、削除、返金など）は、別の注意が必要です。

### よくある間違い

**メニューから管理者リンクを隠しているだけ。** URLを知っている人は入れます。

**「管理者かどうか」をブラウザ側だけで判定している。** 開発者ツールから状態を書き換えれば突破できます。

**権限の判定材料を、ユーザーが書き換えられる場所に持たせている。** たとえば「ユーザーテーブルの `is_admin` 列」を、そのユーザー自身が更新できるRLSポリシーになっていれば、自分で自分を管理者にできます。

### 正しい形

1. 管理者判定は**サーバー側**で行う
2. 管理者用のAPI**すべての入口**で権限を確認する（1箇所でも漏れると意味がない）
3. 権限の情報は**ユーザーが書き換えられない場所**に持たせる（データベースの列＋書き込み禁止のRLS、またはIDトークンのカスタムクレーム）

---

## 7. AIに貼る修正指示

確認と修正をまとめてAIに任せる場合の指示文です。

```text
このアプリの、IDを受け取ってデータを返す/更新する/削除するすべての処理
（Route Handler、Server Action、API、ページのデータ取得）を洗い出して、
1つずつ次を確認し、表にして報告してください。

(1) サーバー側で「そのデータがログイン中のユーザーのものか」を検証して
    いるか
(2) 検証していない場合、他人のIDを指定すると何が起きるか
(3) フロントエンドで表示を隠しているだけの箇所はどれか

そのうえで、検証していない箇所すべてに所有者チェックを追加してください。
データ取得のクエリ自体に所有者の条件を含める形（後からアプリ側で弾く
のではなく）にしてください。読み取りだけでなく、更新と削除も対象です。

あわせて、管理者向けの機能を全部洗い出し、それぞれサーバー側で権限
チェックが行われているか確認してください。権限の判定材料をユーザー
自身が書き換えられる状態になっていないかも確認してください。

私はコードが読めないので、各修正について「何ができてしまう状態だった
のか」を日本語で1行ずつ説明してください。
```

---

## 8. 修正したあと、もう一度確認する

修正後は、必ず**手順4の確認をやり直してください**。

AI は「修正しました」と報告しますが、実際に効いているかは別の話です。Bさんでログインして、AさんのURLを開く。エラーになれば直っています。

これは1分で終わる確認で、修正が本当に効いたかを判定できる唯一の方法です。「AIが直したと言ったから直っている」は、この領域では信用できません。

---

## まとめ

- 認証（誰か）と認可（してよいか）は別の仕事。AIが書き漏らすのは認可
- URLの番号を1つ変えて他人のデータが見えるのは、OWASPがAPIリスク第1位に挙げる典型的な穴
- 一覧は正しく絞られるのに詳細は素通し、という非対称が起きる。だから動作確認では気づけない
- 確認はブラウザだけでできる。アカウント2つで、片方のURLをもう片方で開く
- 「隠す」は防御ではない。判定はサーバー側で、RLSと合わせて二重に

この項目は、[RLS](/blog/supabase-rls-explained-for-non-engineers)と並んで、公開前に必ず潰すべき4つのうちの1つです。全体像は [AIで作ったアプリを公開する前に](/blog/ai-app-pre-launch-guide-for-non-engineers) にまとめています。
