
はじめに
「この機能はUI上の1つのボタンからしか呼ばれないから、細かい権限チェックは省略していい」。こう考えたくなる場面は少なくありません。とくにAI生成コンテンツのように「システムが自動でやっていること」を扱う機能では、通常の権限チェックを一部スキップしたくなる誘惑があります。
しかし、フロントエンドの導線がどれだけ限定されていても、APIはそのボタンを経由せずに直接叩けます。今回取り上げるのは、実運用しているサービスで見つかった「AI生成フラグによる権限チェックのバイパス」という、よくある脆弱性パターンの修正例です。特定の型番や製品名には触れず、権限設計の考え方として汎用化して整理します。
こんな人におすすめ
- API のエンドポイントで「特定用途だけを想定した条件分岐」を書いたことがある方
- 権限チェックのロジックにフラグベースの例外処理を入れている方
- セキュリティレビューやテスト設計の観点を強化したい方
- 「差分は小さいのに影響が大きい」修正のパターンをストックしたい方
本論
問題のあった設計:「AI生成なら権限チェック不要」
掲示板的なアプリで、AIが生成した回答を「自分の投稿にワンクリックで添付する」機能があったとします。この添付処理では、通常の「返信作成権限(reply:create)」チェックをスキップしていました。理由は「AI生成コンテンツは投稿者本人しか使わない機能だから、権限チェックは不要」という前提です。
問題は、この前提がAPIレベルでは何の保証もされていない点にあります。次のようなコードを見てみましょう。
// 修正前のイメージ
if (isAiGenerated) {
// AI生成コンテンツなら権限チェックをスキップ
} else {
const permissionResult = checkPermission(user, 'reply:create');
if (!permissionResult.allowed) {
throw new PermissionError(permissionResult.reason || 'Permission denied');
}
}
isAiGenerated はリクエストボディに含まれる単なるブール値です。フロントエンドの導線を通さず直接APIを叩けば、誰でもこの値を true にできます。結果として、本来は返信権限を持たないユーザーでも「AIバッジ付き」で任意の内容を他人の投稿に書き込める状態になっていました。
修正の方針:フラグではなく「状況」を検証する
修正の考え方はシンプルです。「AI生成なら権限チェック不要」ではなく、「AI生成 かつ 対象リソースの所有者本人なら権限チェック不要」に条件を強化します。フラグそのものを信頼するのではなく、フラグが指す状況が本当に成立しているかをサーバー側で検証する、という発想です。
// 権限チェック
// isAiGenerated は「AI生成コンテンツを自分のリソースに添付する」用途のみ
// 通常の作成権限チェックをスキップしてよい。所有者以外が使うと isAiGenerated を悪用して
// 任意内容を「AI」バッジ付きで他人のリソースに書き込める(権限バイパス)ため、
// 対象リソースの所有者本人であることを必須にする。
if (isAiGenerated) {
if (resource.author.id !== user.id) {
throw new PermissionError('AI生成フラグは所有者本人のみ使用できます');
}
} else {
const permissionResult = checkPermission(user, 'reply:create');
if (!permissionResult.allowed) {
throw new PermissionError(permissionResult.reason || 'Permission denied');
}
}
修正差分としては、if文の条件分岐を1つ足しただけです。脆弱性の重大さと修正コストの小ささが釣り合わないケースは、権限まわりのバグでは珍しくありません。
テストは「バイパス成功」だけでなく「バイパス拒否」も書く
権限バイパスの修正では、正常系のテストだけでは不十分です。「他人がフラグを使って権限チェックを回避しようとして拒否される」テストを追加することで、将来同じ穴が再発しないことを保証できます。
it('403: フラグで所有者以外が権限チェックをバイパスすることはできない', async () => {
const otherUserId = '550e8400-e29b-41d4-a716-446655440021';
const mockUser = {
id: otherUserId, // リソースの所有者とは別人
name: '別のユーザー',
role: 'user' as const,
memberLevel: 'free', // 通常なら作成不可の権限
};
const body = {
content: '任意の内容をAIバッジ付きで偽装投稿',
isAiGenerated: true,
};
const response = await POST(request, { params: Promise.resolve({ topicId: 'topic-123' }) });
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toBe('AI生成フラグは所有者本人のみ使用できます');
expect(mocks.createReply).not.toHaveBeenCalled();
// 通常の作成権限チェックも行わない(専用のガードで弾く)
expect(mocks.checkPermission).not.toHaveBeenCalled();
});
checkPermission が呼ばれていないことまで確認しているのがポイントです。専用のガードで早期に弾いていることを保証することで、「別の経路から権限チェックをすり抜けていないか」まで検証できます。
つまづきやすいポイント
- 「特定用途専用のショートカット」は往々にして汎用エンドポイントに紐づく: UI上は1つのボタンからしか呼ばれないつもりでも、APIレベルでは誰でも同じフィールドをリクエストに含められます。フロントの導線だけを信用した権限設計は成立しません。
- スキップ条件は「フラグの有無」ではなく「状況の成立」で判定する: フラグを信じるのではなく、フラグが主張する状況(今回で言えば所有者本人であること)をサーバー側で検証する必要があります。
- 修正のタイミングで周辺の権限テストも棚卸しする: 1つの権限バグを直すついでに、隣接する権限まわり(削除系エンドポイントなど)のテストカバレッジを底上げしておくと、同種の見落としを早期に発見しやすくなります。
- 「if (flag) { skip check }」というパターンは横展開しやすい: 同じ設計思想で書かれた別のエンドポイントにも、同種のバイパスが潜んでいないか横断的に洗い出す価値があります。
まとめ
「特定の限定用途だけを想定したフラグ」による権限チェックのスキップは、想定外の入力経路から悪用されやすい典型的な脆弱性パターンです。
フロントエンドの導線を信用せず、APIに届いた値は誰でも書き換えられるものとして扱う姿勢が重要になります。
修正自体は小さな条件分岐の追加で済むことが多いため、権限まわりのコードレビューでは「このフラグは本当にサーバー側で検証されているか」を意識的にチェックすると良さそうです。

