定義と結論
セキュリティが弱いAI生成コードとは、正常な入力では動いても、悪意ある入力、権限の違い、秘密情報の漏えい、失敗時の状態を十分に扱っていないコードです。AIだけが弱いコードを作るわけではありませんが、短い指示から一般的な実装例を生成すると、プロジェクト固有の脅威や運用条件が抜けやすくなります。
AI生成コードは「動くか」だけでなく、入力、認証、認可、秘密情報、失敗時、副作用の境界でレビューします。
この記事のコードは危険性を理解するための最小例であり、本番用の完成実装ではありません。安全な実装はDB、フレームワーク、認証方式によって変わるため、公式資料と既存設計を確認します。
なぜAI生成コードで確認が必要なのか
AIは質問文にない要件を知りません。「ユーザー取得APIを作って」と頼んでも、誰が誰の情報を読めるか、どの項目を返すかは決まりません。短いサンプルでは入力検証やエラー処理が省かれる場合もあります。
また「安全にして」という追加指示だけでは不十分です。AIの説明はレビューやテストの代わりになりません。生成後に人間が脅威を整理し、公式ガイド、静的解析、失敗系・権限テストで確認します。
登場人物・守る対象・境界
未ログイン利用者、別アカウント、管理者、外部サイトも考えます。守る対象は個人情報、認証情報、投稿、ファイル、決済情報、可用性、監査ログです。
境界は、ブラウザからAPI、APIからDB、一般ユーザーから管理者機能へ移る場所です。信頼境界を越えて入る値は、送信元で検証済みでも、受け取る側で再検証します。 レビュー担当者は設定、権限、ログ、依存ライブラリも確認します。
レビューの基本フロー

- 守る資産、利用者、信頼境界を整理する
- 正常系、失敗系、権限違いを決める
- APIや設定を公式資料と照合する
- 最小権限と安全な失敗へ修正する
- テスト、静的解析、人のレビューを行う
パターン1:SQLを文字列連結する
危険な例です。
const query = `SELECT * FROM users WHERE email = '${email}'`;
const user = await db.query(query);
入力がSQL構文として解釈され、SQLインジェクションにつながります。引用符の手作業置換では安全にできません。
パラメーター化クエリを使います。
const result = await db.query(
"SELECT id, email FROM users WHERE email = $1",
[email]
);
記法はDBドライバーで異なります。ORMでも生SQLや文字列補間は危険になり得ます。入力検証だけに頼らず、値とSQL命令を分離し、DB権限も最小にします。
パターン2:認証だけで認可していない
app.get("/api/users/:id", requireLogin, async (req, res) => {
const user = await getUser(req.params.id);
res.json(user);
});
ログインさえすればURLのIDを変えて他人の情報を読める可能性があります。認証は「誰か」、認可は「その人がこの対象へこの操作をしてよいか」の確認です。
app.get("/api/users/:id", requireLogin, async (req, res) => {
if (req.user.id !== req.params.id) {
return res.status(403).json({ message: "forbidden" });
}
const user = await getUser(req.params.id);
res.json({ id: user.id, displayName: user.displayName });
});
この例も「本人だけが読める」という仕様の場合に限ります。管理者や保護者ロールがあるなら、方針を一か所に集約し、各ルートで一貫して適用します。返却項目も必要最小限にします。
IDを推測しにくいUUIDへ変えても認可は必要です。識別子の複雑さはアクセス許可ではありません。
パターン3:入力をそのまま保存・更新する
app.post("/api/users", async (req, res) => {
const user = await createUser(req.body);
res.json(user);
});
req.body全体を保存処理へ渡すと、想定外の型、極端に長い文字列、role: "admin"のような更新してはいけない項目まで受け取る恐れがあります。クライアント側バリデーションは改変できるため、サーバー側でも許可項目を明示します。
const input = {
displayName: req.body.displayName,
bio: req.body.bio,
};
その上で型、必須、長さ、形式、列挙値を検証します。HTMLとして表示する値は、保存時の検証だけでなく出力先に応じたエスケープが必要です。ファイルなら、サイズ、実際の内容、保存名、保存先、公開範囲も確認します。
パターン4:トークンをlocalStorageへ長期保存する
localStorage.setItem("token", accessToken);
localStorageは同じオリジンのJavaScriptから読めます。XSSが起きると攻撃コードにも読み取られる可能性があります。ただし「必ずCookieにすれば安全」とも限りません。CookieはHttpOnlyでJavaScriptからの読み取りを防げますが、自動送信されるためCSRF対策やSameSite、Secure、送信範囲の設計が必要です。
サーバーセッション+HttpOnly Cookie、メモリ上の短命トークン、localStorageにはそれぞれ異なるリスクがあります。認証ライブラリの公式推奨と脅威モデルに従い、トークンを短命にし、権限を絞り、失効方法を用意します。
パターン5:内部エラーをそのまま返す
try {
await saveUser(input);
} catch (error) {
res.status(500).json({ error: String(error), stack: error.stack });
}
スタックトレース、ファイルパス、テーブル名、外部サービスの応答、秘密情報が利用者へ露出する可能性があります。一方、エラーを完全に握りつぶすと運用者が調査できません。
try {
await saveUser(input);
} catch (error) {
req.log.error({ err: error, requestId: req.id }, "save failed");
res.status(500).json({
message: "internal server error",
requestId: req.id,
});
}
利用者には一貫したメッセージ、運用側にはアクセス制御されたログを残します。ログにもpassword、token、Cookie、不要な個人情報を出しません。
パターン6:CORSを認証の代わりにする
開発中にすべて許可する設定を入れ、そのまま本番へ残す例があります。CORSはブラウザが異なるオリジンのレスポンスをJavaScriptへ公開するルールです。APIへのアクセス権限そのものではなく、curlやサーバー間通信を止めません。
許可Originを環境ごとに明示し、資格情報が必要か確認します。Cookieを使う場合はCookie属性とCSRF対策も設計します。
パターン7:秘密情報をコードや画面へ埋め込む
const API_KEY = "service-secret-key";
ブラウザへ配信されるJavaScript、公開リポジトリ、ログに入った秘密は、後で文字列を消しても取得済みの人から回収できません。クライアントに必要な公開識別子と、サーバーだけが持つ秘密鍵を区別します。
秘密は秘密管理機能からサーバー側で読みます。漏えいした可能性があれば、コードから消すだけでなく、鍵を失効・ローテーションして利用履歴を確認します。
具体例:AI製プロフィールAPIを確認する
AIが作ったプロフィール更新APIをレビューするとします。正常系の更新だけでなく、次を確認します。
- 未ログインなら401になるか
- 別ユーザーのIDなら403または仕様上の応答になるか
roleやplanを送っても更新されないか- 文字数上限、空値、不正なJSONを扱えるか
- DBクエリがパラメーター化されているか
- レスポンスにpassword hashや内部項目がないか
- ログにリクエストbody全体を出していないか
- 同時更新時の仕様が決まっているか
よくある誤解
AIへ「安全に」と書けば安全になる
守る資産、利用者、権限、環境が指定されなければ、必要な対策を決められません。生成後の検証も必要です。
入力をサニタイズすれば何にでも使える
SQL、HTML、URL、シェルでは安全な扱いが異なります。利用先に合う構造化API、パラメーター化、出力エスケープを使います。
フレームワークを使えば自動的に安全
安全な初期値があっても、生SQL、危険なHTML挿入、認可漏れ、設定ミスは残ります。
開発環境だから秘密を置いてよい
リポジトリや共有ログから漏れる可能性があります。開発用でも権限を絞った別資格情報を使います。
エラーが出なければ攻撃も防げた
攻撃者の操作が成功しても、アプリが200を返す場合があります。保存結果、権限、情報露出を確認します。
ベストプラクティス
- 生成差分を小さくし、依頼外の設定変更を確認する
- DB、認証、暗号は公式ライブラリの高水準APIを使う
- deny by defaultと最小権限を採用する
- 依存関係を固定し、既知の脆弱性を監査する
- 正常系に加え、未認証、権限違い、境界値をテストする
- 秘密情報をAI、コード、ログ、画面へ出さない
- セキュリティ上の判断と残るリスクを記録する
危険なコードを一行直すだけでなく、そのコードを通した設計・設定・テストの不足まで直すことが再発防止になります。
デバッグ・確認方法
最初に差分を読み、ネットワーク、ファイル、DB、認証に触れる箇所を抽出します。次にプロジェクト既存のlint、型チェック、テスト、依存監査を実行します。警告を機械的に無効化せず、データの入口と行き先を追います。
権限テストでは少なくとも、未ログイン、所有者、別ユーザー、管理者を分けます。入力テストでは空値、最大長の前後、型違い、不正形式を使います。ブラウザのNetworkでstatus、headers、bodyを確認し、サーバーログはrequest IDで照合します。本番データや実在の秘密情報をテストに使いません。
AIへの追加確認は補助として使えます。
この差分の信頼境界を列挙してください。
入力検証、認証、認可、秘密情報、ログ、失敗時の副作用について、
未確認の前提と必要なテストを分けて示してください。
ただし、AIの自己レビューだけで承認しません。公式資料、実行結果、テスト、人間の差分レビューを証拠にします。
まとめ
AI生成コードでは、SQL文字列連結、認可漏れ、入力の一括受け渡し、トークン保存、詳細エラー露出、CORS誤用、秘密情報の埋め込みを重点的に確認します。正常系が動くことと、安全であることは別です。
守る対象と信頼境界を明確にし、最小権限、安全なAPI、失敗系・権限テスト、公式資料、人間のレビューを組み合わせます。AIは実装と観点出しを速められますが、安全性を保証する主体にはなりません。