Claude Codeをメインに使って、シンプルなファイル転送サービスを作っていました。 機能を少しずつ足しながら開発していたのですが、ある程度形になったところでセキュリティレビューをさせてみたら、セキュリティというより設計の不整合がいくつか出てきました。
出てきた問題
たとえばこういう話です。
認証コードを管理するテーブルに purpose(用途)というカラムがありました。ファイルDL用・ログイン用・登録用の区別に使う想定でAIが用意してくれたやつです。
ところが実際のコードを見ると、そのカラムで絞り込みをしていなかった。 AIが定義だけして使っていない状態です。
さらにログイン用の認証コードのキーが、全ユーザー共通の固定文字列になっていました。
create_auth_code('__login__', 'login') # 全ユーザー共通
ファイルのダウンロードはファイルごとに一意なIDを使っているのに、ログインは '__login__' という固定文字列です。
同時にログインしている別ユーザーのコードで認証が通ってしまうと気づきました。
もう一つ。ログイン失敗を記録する関数の引数が ip という名前でした。
def _record_attempt(db, ip):
...
「IPアドレスで管理してるんだな」と思ってDBの中身を見ると、ip カラムに '__sender__' という文字列が並んでいました。「?」となって呼び出し元を確認すると、
実際に渡していたのは固定文字列で、IPアドレスではなかった。
IPを渡すつもりで作った関数に、固定文字列を渡してしまったパターンです。
なぜこうなったか
auth_codes テーブルはAIが最初、ファイルダウンロード専用として作りました。その後ログインMFAを追加するとき、「同じテーブルを使い回せばいいか」と流用したのが始まりです。purpose カラムもそのタイミングで追加されたのですが、後から見ると使われていなかった。
「なんで定義したのに使ってないんだ?」と思って掘り下げてみると、後から機能を追加するとき、AIは前の設計の意図を覚えていないという構造的な問題に気づきました。会話ごとにコンテキストがリセットされるので、「なぜこのカラムを作ったか」という背景が次の会話に引き継がれません。これはモデルのグレードの問題というより、インクリメンタルな開発との相性の話だと思っています。
ちなみに最終的には auth_codes の設計ごと見直して、ログイン・登録のOTPは users テーブルに持たせる形に作り直しました。修正しながら「そもそも別にすべきだったな」と思ったやつです。
やったこと
テーブル設計を整理しました。
auth_codes→ ファイルダウンロード専用に絞る(purposeカラムを削除)- ログインOTP →
usersテーブルにlogin_code/login_code_expires_atカラムを追加 - 登録OTP → 同様に
usersにregister_code/register_code_expires_atを追加
こうすることで auth_codes のトークンは常にファイルUUIDだけになって、役割がはっきりしました。
そしてこういう問題が繰り返さないように、Claude Codeのカスタムコマンドを作りました。
コマンドは「設計の整合性・セキュリティ・データ安全性・コード品質」の4観点でレビューを走らせます。今回出てきた「カラムが定義されているのに使われていない」「固定文字列をIDに使っている」「引数名と実際の内容が一致していない」といったチェックがそのまま項目に入っています。
/pre-merge-check で使えます
プロジェクトによって気にすべき観点は変わるので、自分のコードに合わせて項目を足したり削ったりしてみてください。
感想
AIで開発するとき、コード自体の品質は意外と高いです。SQLインジェクションやXSSはちゃんと対策されていたし、セキュリティの基本的なところは問題なかった。
問題が起きやすいのは「設計の一貫性」でした。
一度に書いたコードは整合しているけど、後から足した部分が既存設計と微妙にずれてくる。これはAI開発の特性というより、インクリメンタルな開発全般に言える話ではあるんですが、AIの場合はコンテキストのリセットがその傾向を強める気がします。
定期的にレビューを回す習慣と、プロジェクト固有のチェックリストを育てていくのが今のところ一番現実的な対策かなと思っています。