社内文書を検索対象にしたRAG(Retrieval-Augmented Generation、検索結果をもとにLLMが回答を生成する仕組み)を構築中、またはこれから構築しようとしているエンジニアに向けた記事です。権限管理をアプリ層のフィルタで済ませている設計が、実際どの程度の確率で情報漏洩を起こすのかを整理しました。
72件の社内文書を模した合成コーパス(検証用に作ったテキスト集合)を使い、検索・推論・ログ記録の全体を計測した検証プロジェクトがあります。そこで示された数字が強烈です。「埋め込み生成→上位k件取得→引用付きで回答」という、フレームワークのチュートリアル通りに午後一つで組めるベースライン実装が、権限チェック用の質問10問中10問で、聞く権利のない情報を漏らしました。
何が起きるか
検証で特に象徴的だったのは、権限が「公開情報のみ」のはずの契約社員が「決済APIをロールバックする方法」を尋ねたケースです。モデルは何の迷いもなく、社内オンコール向けランブック(障害対応手順書)に書かれたdeployctlコマンドをそのまま返しました。
これは特殊な攻撃でも、巧妙なプロンプトインジェクション(指示を埋め込んで挙動を乗っ取る攻撃)でもありません。ベクトル検索で上位にヒットした文書をそのままコンテキストに詰めて回答させる、ごく標準的なRAG実装の既定動作です。言い換えると、「権限チェックをアプリケーションコードの後工程でやる」設計そのものが、デフォルトで漏洩を起こします。
なぜ起きるか
原因を分解すると、ポイントは「どの層で権限を enforce(強制)しているか」に集約されます。
多くのRAG実装は、こういう順序で処理を組みます。
1. ユーザーの質問をベクトル化する
2. ベクトルDB(pgvectorやPineconeなど)から類似度上位の文書チャンクを取得する
3. 取得したチャンクをLLMのコンテキストに詰めて回答生成を依頼する
権限チェックをPythonの条件分岐などで「取得した後に」フィルタしようとする設計は、構造的に脆弱です。理由は単純で、フィルタは「誰かが書き忘れる・迂回する・順序を間違える」コードだからです。たとえばエラー処理の都合でフィルタ関数の呼び出し順を入れ替えただけで、チェックが素通りする事故は容易に起こり得ます。
もう一つ重要なのは、「漏洩はどの時点で発生したとみなすべきか」という考え方です。権限外のチャンクが一度でもモデルのコンテキストに入った時点で、情報はすでに露出しています。モデルへのプロンプト、トレースログ、監視システムが記録するプロンプト内容——これらすべてに権限外の情報が記録され得るからです。回答テキストだけを後からスコアリングして「漏れていないか」を判定する方式は、漏洩そのものではなく「隠蔽の巧拙」を採点してしまうことになります。
加えて、エンティティ(権限情報)のスコープ管理にも落とし穴があります。トランザクション単位で権限を設定する設計の場合、コネクションプーリングを使っていると、ある接続に設定した権限情報が次のリクエストに持ち越されるバグが起こり得ます。これはパフォーマンス最適化の皮をかぶった権限昇格(privilege escalation)です。接続を使い回すたびに前のユーザーの権限が残っていないか、明示的にテストで確認する必要があります。
自分のプロジェクトが該当するか確認する方法
以下の観点で、自分のRAGパイプラインを点検してみてください。
- 検索処理(retrieval)のコードに、ユーザーの権限情報を条件にSQLのWHERE句やベクトルDBのフィルタ条件を直接組み込んでいるか。それとも「全件検索してからPythonで絞る」実装になっていないか
- 権限チェックのロジックが、アプリケーションコードとデータベースのどちらに存在するか。アプリ側にしかない場合、コードの変更・リファクタ・バグで簡単に迂回され得る
- ベクトルDBにPostgreSQLのRLS(Row-Level Security、行単位でアクセス制御する機能)のような、DB自体が強制する権限機構を使っているか。pgvector拡張を使っているなら
CREATE POLICYでRLSを設定できるか確認する価値があります - コネクションプーリング(PgBouncerなど)を使っている場合、トランザクション単位で設定したロールやパラメータが、プール経由で次のリクエストに漏れ出していないかをテストしているか
- プロンプトインジェクション対策のテストを、権限チェックのテストと混同していないか。この2つは別物です
確認コマンドの例として、PostgreSQLでRLSが有効になっているテーブルは次のように調べられます。
SELECT relname, relrowsecurity
FROM pg_class
WHERE relkind = 'r' AND relrowsecurity = true;relrowsecurityがfalseのまま機密文書を保持するテーブルがあれば、検索処理は現状「DBが守ってくれない」状態にあります。
対策の手順
対策の軸は「権限の強制をデータベース側に移す」ことです。具体的には次の手順になります。
1. ベクトル検索用のテーブルに、文書の閲覧権限に対応するグループやロールの列を追加する
2. PostgreSQLであれば制限されたロールを作成し、そのロールに対してRLSポリシーを設定する。アプリケーションはこの制限ロールで接続し、権限を持たない行はそもそも検索結果の候補集合(candidate set)に入らないようにする
3. 検索処理自体のテストを、アプリケーションを経由せず制限ロールで直接DBにクエリを投げる形で書く。期待される可視件数は、コーパスの元ファイルから独立に計算し、DB側の実装と同じロジックを使い回さない。実装のバグとテストの期待値が同じ前提を共有していると、バグに気づけません
4. トランザクションごとの権限設定が、コネクションプールを越えて残留しないかを明示的にアサートするテストを追加する
5. 権限外のチャンクが検索結果に含まれていないかを「検索の時点」で計測する。回答文のテキストを事後チェックする方式は補助的な手段にとどめる
プロンプトインジェクション対策については、別の考え方が必要です。埋め込み型の攻撃(偽の権限ヘッダーや「指示を無視せよ」という文言を文書に仕込む手口)に対して、引用の正確さや拒否判定といった「モデルの振る舞いに依存する防御」は、モデルやプロンプトが変わるたびに揺らぎます。一方、RLSやルーティングポリシー、追記専用の監査ログ(append-only ledger)のような「モデルの振る舞いに依存しない防御」は、攻撃を受けても崩れません。インジェクションによってモデルにOVERRIDE-ACCEPTEDと言わせることはできても、権限のない行をPostgreSQLから返させることはできない、という切り分けが要点です。
まとめ
社内RAGを設計・レビューする際は、権限チェックが「アプリ層のフィルタ」なのか「DB層の強制」なのかをまず確認してください。
- 検索処理のクエリに権限条件が組み込まれているか、
pg_classでrelrowsecurityを確認する - 検索テストはアプリを経由せず、DBに対して直接権限付きクエリを投げる形で書く
- コネクションプールを使っているなら、トランザクション単位の権限が越境していないかをテストで検証する
- プロンプトインジェクション対策と権限管理は別のレイヤーの問題として、それぞれ個別に計測する
フィルタをPythonに書くか、ポリシーをデータベースに書くか。この一点の選択が、監査で問われたときの説明責任の重さを大きく左右します。