AIが生成したコードをレビューするチェックリスト|動くのに危険な実装を見抜く

AIが生成したコードをレビューするチェックリスト|動くのに危険な実装を見抜く AI開発

ChatGPTやClaude、Gemini、AIコーディングエージェントを使うと、API、SQL、認証処理、ファイル操作まで短時間で実装できるようになりました。

AIが生成したコードを実行すると、正常に動くことも珍しくありません。

  • ビルドが通る
  • テストデータでは動く
  • 期待したJSONが返る

という状態と、

  • 本番環境へ安全に公開できる

という状態は別です。

たとえばユーザーIDをURLから受け取ってデータベースを検索するAPIは、正常なユーザーでテストすれば問題なく動きます。

しかし別ユーザーのIDへ変更するだけで他人のデータを取得できるなら、重大なアクセス制御の不備です。

SQLへ文字列を直接連結するコードも、通常の検索ワードなら正常に動きます。問題が発覚するのは、攻撃用の文字列を入力されたときです。

2026年8月時点の最新OWASP Top 10:2025でも、Broken Access Controlが最上位のリスクとなっており、Software Supply Chain Failures、Injection、Authentication Failures、Security Logging and Alerting Failuresなどが主要リスクとして挙げられています。

AIが生成したコードをレビューするときは、「このコードは動くか」だけでなく、攻撃者が入力値、URL、権限、実行順序を自由に変更した場合でも安全かという視点が必要です。

この記事ではTypeScriptとNode.jsを中心に、AIが生成したコードを本番採用する前に確認したいポイントを解説します。

スポンサーリンク
  1. 最初に「正常系」ではなく「攻撃者なら何を変えるか」を考える
  2. 認証があるだけで安全だと思わない
  3. 管理者判定をフロントエンドだけで行っていないか確認する
  4. 外部入力を型だけで信用していないか確認する
  5. SQLを文字列結合していないか確認する
  6. ORMを使っているからSQL Injectionはないと思わない
  7. child_processへユーザー入力を渡していないか確認する
  8. ファイルパスをそのまま結合していないか確認する
  9. URLをそのままfetchしていないか確認する
  10. APIキーやパスワードがコードへ埋め込まれていないか確認する
  11. NEXT_PUBLICなどの公開Environment Variableへ秘密情報を入れていないか確認する
  12. request body全体をログへ出していないか確認する
  13. エラー内容をそのままユーザーへ返していないか確認する
  14. catchした結果、安全側ではなく成功扱いになっていないか確認する
  15. 外部APIにタイムアウトが設定されているか確認する
  16. Retryを追加したことで二重処理にならないか確認する
  17. 同時実行された場合を確認する
  18. Unique ConstraintをApplicationのif文だけで代用していないか確認する
  19. limitやpageSizeを無制限に受け取っていないか確認する
  20. AI APIを呼ぶコードでは料金上限も確認する
  21. 暗号用途でMath.random()を使っていないか確認する
  22. パスワードを独自方式で暗号化していないか確認する
  23. dangerouslySetInnerHTMLなどの危険な逃げ道を確認する
  24. CORSをとりあえず全許可にしていないか確認する
  25. エラーを直すためにSecurity機能を無効化していないか確認する
  26. AIが追加した依存パッケージをそのままインストールしない
  27. package-lock.jsonを勝手に削除していないか確認する
  28. 「最新版へ全部更新」で直していないか確認する
  29. デバッグ用Endpointが残っていないか確認する
  30. コメントアウトした認証処理が残っていないか確認する
  31. レスポンスへ必要以上のデータを返していないか確認する
  32. Mass Assignmentになっていないか確認する
  33. 削除処理がGETになっていないか確認する
  34. 削除や送信にHuman in the Loopが必要ではないか確認する
  35. AIのコメントを仕様だと思わない
  36. TODOがSecurity処理を先送りしていないか確認する
  37. Unit Testが成功するだけでは十分ではない
  38. AI自身にレビューさせるだけで完了しない
  39. Diffが大きすぎる場合はいったん分割する
  40. 最後にコードを上から読むのではなくデータの流れを追う
  41. AI生成コードをレビューする実践的な順番
  42. 関連記事
  43. AI生成コードのレビューに関するよくある質問
  44. まとめ

最初に「正常系」ではなく「攻撃者なら何を変えるか」を考える

AI生成コードをレビューするときに最初に変更したいのが、テストする視点です。

たとえばAIが次のAPIを作ったとします。

NG:注文の所有者を確認していないAPI
app.get(
  "/api/orders/:id",
  async (req, res) => {
    const order =
      await db.order.findUnique({
        where: {
          id: req.params.id,
        },
      });

    res.json(order);
  },
);

存在する注文IDを入力すれば正常にJSONが返るため、機能テストだけなら合格してしまいます。

しかし確認すべきなのは、

  • 自分とは別のユーザーの注文IDを指定したらどうなるか

です。

注文IDだけで検索しているため、認証済みユーザーと注文の所有者を照合していません。

このようなアクセス制御の問題はOWASP Top 10:2025でも最重要カテゴリとして扱われています。

AI生成コードのレビューでは、正常な入力を1回通して終わりにせず、入力値、ユーザーID、URL、HTTPメソッド、権限、処理順序を変更した場合を確認します。

認証があるだけで安全だと思わない

認証と認可は別です。

ログインしているかどうかを確認していても、そのユーザーが対象データを操作できるとは限りません。

先ほどの注文APIを修正します。

OK:注文の所有者を検索条件に含める
app.get(
  "/api/orders/:id",
  requireLogin,
  async (req, res) => {
    const order =
      await db.order.findFirst({
        where: {
          id: req.params.id,
          userId:
            req.user.id,
        },
      });

    if (!order) {
      return res
        .status(404)
        .json({
          error:
            "Order not found",
        });
    }

    return res.json(order);
  },
);

重要なのは、

id: req.params.id

だけではなく、

userId: req.user.id

も検索条件へ入れている点です。

ユーザーから受け取った、

req.body.userId

を所有者判定へ利用するのも避けます。

攻撃者が自分で値を変更できるからです。

認可判定には、セッションや検証済みアクセストークンなど、サーバー側で信頼できる情報を使用します。

管理者判定をフロントエンドだけで行っていないか確認する

AIが管理画面を生成すると、React側だけで、

フロントエンドだけの管理者判定(UIを隠すだけ)
if (
  user.role !== "admin"
) {
  return null;
}

として管理ボタンを隠すコードを作ることがあります。

これはUI制御としては使えますが、認可にはなりません。

攻撃者はブラウザのボタンを押さず、APIへ直接リクエストできます。

  • DELETE /api/users/123

バックエンドでも権限を確認します。

OK:バックエンドでも権限を確認する
app.delete(
  "/api/users/:id",
  requireLogin,
  async (req, res) => {
    if (
      req.user.role !==
      "admin"
    ) {
      return res
        .status(403)
        .json({
          error:
            "Forbidden",
        });
    }

    await deleteUser(
      req.params.id,
    );

    return res.sendStatus(204);
  },
);

画面上から機能を隠す処理と、APIでアクセスを拒否する処理を混同しないことが重要です。

外部入力を型だけで信用していないか確認する

TypeScriptを使っていると、

型定義だけでは実行時の値は保証されない
type CreateUserInput = {
  email: string;
  age: number;
};

と定義しただけで安全になったように見えます。

しかしTypeScriptの型は、インターネットから届いたJSONを実行時に検証してくれるわけではありません。

次のようなコードは危険です。

NG:型アサーションだけで実行時検証をしていない
const input =
  req.body as
    CreateUserInput;

await createUser(
  input,
);

攻撃者は任意のJSONを送信できます。

Runtime Validationを行います。

OK:zodで実行時バリデーションを行う
import { z } from "zod";

const CreateUserSchema =
  z.object({
    email:
      z.string()
        .email()
        .max(254),

    age:
      z.number()
        .int()
        .min(0)
        .max(120),
  });

app.post(
  "/api/users",
  async (req, res) => {
    const parsed =
      CreateUserSchema
        .safeParse(
          req.body,
        );

    if (!parsed.success) {
      return res
        .status(400)
        .json({
          error:
            "Invalid input",
        });
    }

    const user =
      await createUser(
        parsed.data,
      );

    return res.json(user);
  },
);

CWE Top 25でも不適切なInput Validationは重要なSoftware Weaknessとして扱われています。入力値を後工程が期待する形式・範囲に検証することは、SQL、ファイル操作、外部APIなどへデータを渡す前の基本です。

SQLを文字列結合していないか確認する

AI生成コードで特に見つけやすい危険パターンがSQLの文字列連結です。

NG:SQLに変数を直接埋め込んでいる
const sql = `
  SELECT *
  FROM users
  WHERE email =
    '${email}'
`;

const result =
  await pool.query(sql);

通常の、

  • user@example.com

なら正常に動きます。

しかしSQLとして意味を持つ入力を渡されたときに、Queryそのものが変更される可能性があります。

OWASP Top 10:2025ではInjectionがA05に位置付けられ、SQL Injectionなどが含まれています。2025 CWE Top 25にもSQL InjectionとOS Command Injectionが含まれています(SQL InjectionはCWE-89で2位、OS Command InjectionはCWE-78で9位)。

Node.jsからPostgreSQLを利用するなら、値をParameterとして渡します。

OK:値をパラメータとして渡す
const result =
  await pool.query(
    `
      SELECT
        id,
        email,
        name
      FROM users
      WHERE email = $1
    `,
    [
      email,
    ],
  );

レビューするときは、

`SELECT ... ${value}`

のように外部入力がSQL文字列へ直接埋め込まれていないか検索します。

ORMを使っているからSQL Injectionはないと思わない

ORMを利用していても、Raw SQL機能を使えば同じ問題が発生します。

AIは複雑なQueryを実装するときに、ORMの通常APIではなくRaw Queryへ切り替えることがあります。

レビューでは、

  • raw
  • unsafe
  • queryRaw
  • executeRaw

などのAPIを重点的に確認します。

ORMそのものが安全かどうかではなく、ユーザー入力とSQL構文が同じ文字列へ結合されていないかを見ることが重要です。

child_processへユーザー入力を渡していないか確認する

AIコーディングでは、ファイル変換やGit操作を簡単に実装するためにchild_process.exec()が生成されることがあります。

たとえば次のコードです。

NG:exec()にユーザー入力を直接埋め込んでいる
import {
  exec,
} from "node:child_process";

function convertFile(
  fileName: string,
) {
  exec(
    `convert ${fileName} output.png`,
  );
}

fileNameをユーザーが操作できるなら、Shell Commandとして解釈される特殊文字が混ざる危険があります。

Node.jsのexec()はShellを起動してCommandを実行するAPIです。OS Command Injectionは2025 CWE Top 25でも9位に位置する危険なSoftware Weaknessです。

可能ならShell文字列を組み立てず、実行ファイルとArgumentsを分離します。

OK:execFile()でシェルを経由せず実行する
import {
  execFile,
} from "node:child_process";

execFile(
  "convert",
  [
    inputPath,
    outputPath,
  ],
  (error) => {
    if (error) {
      console.error(
        "convert failed",
      );
    }
  },
);

ただし、実行対象、OS、ファイル形式によって追加の注意点があります。

特にWindowsのBatch File処理では、spawnspawnSyncのArgument処理からCommand InjectionにつながるNode.js脆弱性(CVE-2024-27980、CVE-2024-36138)が実際に公開されています。外部入力から実行ファイル名やBatch Fileを自由に選ばせる設計自体を避けるほうが安全です。

ファイルパスをそのまま結合していないか確認する

AIにファイル取得APIを作らせると、次のようなコードが生成されることがあります。

NG:ユーザー入力をそのままパス結合している
const filePath =
  path.join(
    "/app/uploads",
    req.params.file,
  );

return res.sendFile(
  filePath,
);

問題は、

  • ../../

のようなパスを指定された場合です。

Path Traversalは2025 CWE Top 25でもCWE-22として6位に位置しています。

アプリケーションが許可したディレクトリから外へ出ていないことを検証します。

OK:許可ディレクトリの外に出ていないか検証する
import path from "node:path";

const BASE_DIR =
  path.resolve(
    "/app/uploads",
  );

function resolveSafePath(
  input: string,
): string {
  const target =
    path.resolve(
      BASE_DIR,
      input,
    );

  const relative =
    path.relative(
      BASE_DIR,
      target,
    );

  if (
    relative.startsWith("..") ||
    path.isAbsolute(relative)
  ) {
    throw new Error(
      "Invalid path",
    );
  }

  return target;
}

さらに、攻撃者がSymlinkなどを作成できる環境では文字列上のPath Validationだけでは不十分な場合があります。

重要ファイルへアクセスできないOS権限、Container、Sandboxなどと組み合わせます。

URLをそのままfetchしていないか確認する

URL取得機能も注意が必要です。

NG:ユーザー指定のURLへそのままfetchしている
app.post(
  "/api/fetch",
  async (req, res) => {
    const response =
      await fetch(
        req.body.url,
      );

    res.send(
      await response.text(),
    );
  },
);

この実装では、ユーザーが指定したURLへサーバー自身がアクセスします。

外部Webサイトだけでなく、

  • localhost
  • 社内ネットワーク
  • クラウドのMetadata Service

などへアクセスできる構成ならSSRFにつながります。

OWASP Top 10:2025ではSSRFがBroken Access Controlカテゴリに統合されています。

アクセス先が決まっているサービスならAllowlist方式にします。

OK:Allowlistでアクセス先を制限する
const ALLOWED_HOSTS =
  new Set([
    "api.example.com",
    "images.example.com",
  ]);

function validateUrl(
  rawUrl: string,
): URL {
  const url =
    new URL(rawUrl);

  if (
    url.protocol !==
    "https:"
  ) {
    throw new Error(
      "HTTPS only",
    );
  }

  if (
    !ALLOWED_HOSTS.has(
      url.hostname,
    )
  ) {
    throw new Error(
      "Host not allowed",
    );
  }

  return url;
}

任意URLへのアクセスが必要なサービスでは、単純なlocalhost文字列チェックだけでは十分ではありません。

DNS解決後のIP、Private Network、Redirect先、IPv6なども含めた対策と、Network LevelのEgress Controlを検討します。

APIキーやパスワードがコードへ埋め込まれていないか確認する

AIへ、

  • OpenAI APIを呼ぶサンプルを書いて

と依頼した際、説明用のPlaceholderではなく実際のSecretをそのままコードへ貼り付けてしまう運用は避けます。

危険なコードは次のようなものです。

NG:APIキーをコードへ直接埋め込んでいる
const apiKey =
  "sk-real-secret-value";

CWEではHard-coded Credentials(CWE-798)が独立したSoftware Weaknessとして定義されています。GitHubのSecret ScanningもGit History内のAPI Key、Password、TokenなどのHard-coded Credentialを検出対象としており、Push ProtectionではRepositoryへ入る前にSecretを含むPushをBlockできます。ただしPush Protectionは新規にプッシュされる内容が対象で、既にリポジトリ履歴に含まれてしまった過去のコミットまでは遡ってブロックしない点には注意してください。

サーバー側のSecret StoreやEnvironment Variableから取得します。

OK:環境変数から取得する
const apiKey =
  process.env.API_KEY;

if (!apiKey) {
  throw new Error(
    "API_KEY is required",
  );
}

ただしEnvironment Variableへ移しただけで安全対策が完了するわけではありません。

ログ、例外、デバッグ画面、クライアント向けBundleへSecretが流れていないかも確認します。

NEXT_PUBLICなどの公開Environment Variableへ秘密情報を入れていないか確認する

フロントエンドFrameworkでは、Environment Variableの一部がBrowser Bundleへ公開される仕組みがあります。

AIが、

NG:秘密のAPIキーを公開環境変数に入れている
const apiKey =
  process.env
    .NEXT_PUBLIC_API_KEY;

のようなコードを生成した場合、名前の意味を理解せず採用しないようにします。

ブラウザ側で使用できる値は、ユーザーにも取得できる値だと考えます。

秘密のAPIキーで外部サービスを呼び出すなら、

  • Browser
  • 自社Backend
  • External API

という構成にし、秘密情報はBackend側だけで保持します。

request body全体をログへ出していないか確認する

AIはDebugしやすさを優先して、

NG:リクエスト全体をそのままログに出している
console.log(
  req.body,
);

を追加することがあります。

問い合わせフォームなら、

  • 氏名
  • メールアドレス
  • 電話番号
  • 住所
  • パスワード
  • アクセストークン

などがログへ残る可能性があります。

OWASP Top 10:2025のSecurity Logging and Alerting Failuresでは、重要イベントが適切に記録されない問題だけでなく、Sensitive InformationをLogへ挿入するCWEも関連付けられています。

ログへは必要なMetadataだけを出します。

OK:必要なメタデータだけをログに出す
console.info({
  event:
    "user_update",

  userId:
    currentUser.id,

  requestId,

  status:
    "success",
});

前の記事で解説したPIIマスキングと同じく、AI APIのPromptだけではなくApplication Logもデータフローの一部として確認します。

エラー内容をそのままユーザーへ返していないか確認する

次のコードも開発中には便利です。

NG:エラーの詳細をそのままレスポンスに含めている
catch (error) {
  return res
    .status(500)
    .json({
      error:
        String(error),
    });
}

しかしErrorには、

  • SQL
  • 内部Path
  • Host Name
  • ライブラリ名
  • Stack Trace
  • 外部API Response

などが含まれる場合があります。

OWASP Top 10:2025のSecurity Misconfigurationでは、Stack Traceなど過剰に詳細なError Messageの公開もリスクとして挙げられています。

ユーザー向けには一般化します。

OK:詳細はサーバー側でログに残し一般化して返す
catch (error) {
  console.error({
    requestId,
    error:
      normalizeError(
        error,
      ),
  });

  return res
    .status(500)
    .json({
      error:
        "Internal server error",

      requestId,
    });
}

詳細はサーバー側で追跡します。

catchした結果、安全側ではなく成功扱いになっていないか確認する

AI生成コードでは、Errorを消すためだけにcatchが追加されることがあります。

特に危険なのがFail Openです。

NG:権限確認に失敗すると許可扱いになる(Fail Open)
async function canAccess(
  userId: string,
): Promise<boolean> {
  try {
    return await checkPermission(
      userId,
    );
  } catch {
    return true;
  }
}

Permission Serverが停止すると、

return true;

によって全員が許可されます。

OWASP Top 10:2025ではA10としてMishandling of Exceptional Conditionsが新たに設けられ、Error Handling不足、Logical Error、Fail Openなどが対象になっています。

認可確認に失敗したなら、基本的には拒否側へ倒します。

OK:失敗時は拒否側に倒す(Fail Closed)
catch {
  return false;
}

さらに監視用ログを残します。

正常系だけを見ていると、この違いは発見できません。

外部APIにタイムアウトが設定されているか確認する

AIが生成するfetch()は非常に簡潔です。

NG:タイムアウトが設定されていない
const response =
  await fetch(url);

しかし外部サービスが応答しなければ、Application側のRequestも長時間占有される可能性があります。

時間上限を明示します。

OK:AbortSignal.timeout()で上限を明示する
const response =
  await fetch(
    url,
    {
      signal:
        AbortSignal.timeout(
          10_000,
        ),
    },
  );

AI生成コードをレビューするときは、

  • 外部HTTP
  • LLM API
  • DB Query
  • Shell Command
  • MCP Tool

など、Process外へ出る処理にTimeoutがあるかを確認します。

Timeout後に再試行する場合は、前の記事で解説したように副作用と冪等性も確認します。

Retryを追加したことで二重処理にならないか確認する

AIへ、

  • 失敗したら3回再試行するよう修正して

と依頼すると、簡単なRetry Loopを生成できます。

NG:冪等性を考慮しないRetry
for (
  let i = 0;
  i < 3;
  i += 1
) {
  try {
    return await createOrder();
  } catch {
    await sleep(1000);
  }
}

しかしcreateOrder()がServer側では成功し、Responseだけ失われたケースでは、二重注文になる可能性があります。

読み取り処理と書き込み処理を同じRetry Policyにしないことが重要です。

注文、決済、メール送信などはoperationIdやIdempotency Keyを導入します。

OK:Idempotency Keyを付けて再試行しても安全にする
await createOrder({
  operationId,
  productId,
});

「Retryが実装されているから堅牢」ではなく、「Retryされても安全か」を確認します。

同時実行された場合を確認する

AI生成コードは1Requestずつ実行すると正常でも、同時アクセスで壊れる場合があります。

たとえば在庫を確認してから減らす処理です。

NG:確認と更新の間に競合状態(Race Condition)がある
const product =
  await getProduct(
    productId,
  );

if (
  product.stock <= 0
) {
  throw new Error(
    "Out of stock",
  );
}

await updateStock(
  productId,
  product.stock - 1,
);

同じ瞬間に2Requestが実行されると、両方が、

  • stock = 1

を読み取る可能性があります。

その後どちらも購入成功になるかもしれません。

このような処理ではDatabase Transaction、Atomic Update、Unique Constraint、Lockなど、データベース側の整合性機構を利用します。

AI生成コードのレビューでは、

  • 同じAPIを同時に10回呼んだらどうなるか

という視点を入れると、単体テストでは見えない問題を発見しやすくなります。

Unique ConstraintをApplicationのif文だけで代用していないか確認する

次のコードも正常系では動きます。

NG:重複チェックがApplication側の確認だけ
const existing =
  await db.user.findUnique({
    where: {
      email,
    },
  });

if (!existing) {
  await db.user.create({
    data: {
      email,
    },
  });
}

しかし2Requestが同時にexisting === nullを確認する可能性があります。

「重複禁止」が本当に必要なら、Application側の確認だけでなくDatabase側にもUnique Constraintを設定します。

OK:データベース側にもUnique Constraintを設定する
ALTER TABLE users
ADD CONSTRAINT
users_email_unique
UNIQUE (email);

Application Codeは制約違反を適切に処理します。

AIが生成したBusiness Logicを、データベースが保証すべきInvariantと分けて考えることが重要です。

limitやpageSizeを無制限に受け取っていないか確認する

検索APIでは、

NG:limitの上限を確認していない
const limit =
  Number(
    req.query.limit,
  );

const users =
  await db.user.findMany({
    take: limit,
  });

のようなコードが生成されることがあります。

ユーザーが、

  • limit=100000000

を指定したらどうなるでしょうか。

OWASP API Security Top 10ではUnrestricted Resource Consumption(API4:2023)が主要リスクとして挙げられ、CPU、Memory、Storage、Network Bandwidthだけでなく、API連携による課金Resourceの消費も問題として扱われています。

上限をサーバー側で決めます。

OK:サーバー側で上限を決める
const requested =
  Number(
    req.query.limit ??
      20,
  );

const limit =
  Math.min(
    Math.max(
      requested,
      1,
    ),
    100,
  );

Upload Size、生成AIのmax_tokens、Batch Size、同時実行数にも同じ考え方を適用します。

AI APIを呼ぶコードでは料金上限も確認する

生成AIを組み込んだApplicationでは、セキュリティだけでなくCost Abuseも確認します。

たとえば、

NG:認証もRate Limitもなく公開されているAI呼び出しAPI
app.post(
  "/api/generate",
  async (req, res) => {
    return callLlm(
      req.body.prompt,
    );
  },
);

というEndpointを認証なし、Rate Limitなしで公開すれば、第三者に大量利用される可能性があります。

AI APIでは、

  • 認証
  • ユーザー別Rate Limit
  • 最大入力サイズ
  • 最大出力量
  • 1Request料金上限
  • 日次予算

をApplication側で制御します。

ユーザーが指定したモデル名やmax_output_tokensを無条件でAPIへ渡していないかも確認します。

暗号用途でMath.random()を使っていないか確認する

AIにToken生成処理を書かせると、

NG:Math.random()はセキュリティ用途に使えない
const token =
  Math.random()
    .toString(36)
    .slice(2);

のようなコードが生成されることがあります。

UI用のランダム表現と、Authentication TokenやPassword Reset Tokenは別です。

Security-sensitiveなRandom ValueにはNode.jsのCryptographic APIを使用します。

OK:crypto.randomBytes()で生成する
import {
  randomBytes,
} from "node:crypto";

const token =
  randomBytes(32)
    .toString("hex");

または用途に応じて、

OK:crypto.randomUUID()を使う
import {
  randomUUID,
} from "node:crypto";

const operationId =
  randomUUID();

を使います。

AI生成コードに、

  • token
  • nonce
  • reset code
  • session id
  • API key

のような値があれば、生成方法まで確認します。

パスワードを独自方式で暗号化していないか確認する

AIへ認証処理を一式作らせると、独自のPassword Hashing処理が出てくることがあります。

Security-sensitiveな処理では、

  • 暗号アルゴリズムを自作
  • Password Hashを独自実装
  • Encryption Keyをコードへ埋め込み
  • 固定IVを使用

といった実装を避けます。

OWASP Top 10:2025にもCryptographic Failuresが含まれています。

成熟したLibraryやPlatform標準機能を使い、AlgorithmやParameterは現在の公式Security Guidanceに合わせます。

「AIが実装できるから自作する」のではなく、Security Primitiveは可能な限り自作しないことが重要です。

dangerouslySetInnerHTMLなどの危険な逃げ道を確認する

Reactでは通常の、

通常のJSXはテキストとしてエスケープされる
<p>
  {userText}
</p>

ならTextとしてEscapeされます。

しかしAIはHTMLを表示する要件に対し、

NG:外部入力をそのままHTMLとして描画している
<div
  dangerouslySetInnerHTML={{
    __html:
      userContent,
  }}
/>

を生成することがあります。

userContentが外部入力ならXSSにつながる可能性があります。

InjectionはOWASP Top 10:2025の主要カテゴリで、Cross-site Scriptingもその中に含まれています。

HTML表示が本当に必要なのかを最初に確認し、必要なら用途に合ったSanitizationを行います。

「画面にHTMLとして表示できた」ことを成功条件にしないことが重要です。

CORSをとりあえず全許可にしていないか確認する

開発中にCORS Errorが出ると、AIは素早い解決策として広い許可設定を提案することがあります。

たとえば、

NG:CORSをオリジン全許可にしている
app.use(
  cors({
    origin: "*",
  }),
);

です。

公開APIなら意図した設定の場合もあります。

しかしCookieや管理APIを扱うサービスで、「エラーを消すため」に広げた設定をそのまま本番へ入れるのは避けます。

OWASP Top 10:2025ではSecurity MisconfigurationがA02に位置付けられており、不要な機能、過剰な権限、安全でない設定などがリスクとして扱われています。

AIがConfigを変更したときは、

  • なぜその設定が必要なのか

を説明できる状態にしてから採用します。

エラーを直すためにSecurity機能を無効化していないか確認する

AIへ、

  • このエラーを直して

とだけ依頼すると、原因を修正するのではなく安全機能を無効化してエラーを消す回答になることがあります。

たとえば、

  • TLS証明書検証を無効化
  • CSRF保護を無効化
  • CORSを全許可
  • 認証Middlewareを削除
  • SSL検証を無効化
  • Permission Checkを削除

といった変更です。

コードレビューでは、

  • エラーが消えた理由

を確認します。

正しい設定へ修正した結果なのか、安全装置を取り外しただけなのかを区別します。

Security MisconfigurationはOWASP Top 10:2025でも主要リスクです。

AIが追加した依存パッケージをそのままインストールしない

AIに実装を依頼すると、新しいnpm Packageを追加することがあります。

コードだけを見ると、

AIが追加した新しいimport
import {
  someFunction,
} from "some-package";

が自然に見えても、そのPackageを採用してよいかは別問題です。

Package名が正しいか、公式・想定したPackageなのか、Maintainer、更新状況、依存関係、Install Script、Licenseなどを確認します。

OWASP Top 10:2025ではSoftware Supply Chain FailuresがA03へ位置付けられており、Third-party Code、Tools、Dependenciesなどに起因する問題が対象です。

npm Projectでは Dependencyの既知脆弱性を、

npm audit

で確認できます。

npm公式ドキュメントでも、npm auditはProjectのDependenciesについて既知のSecurity Vulnerabilityを調査するための機能とされています。

ただしnpm auditが0件だからPackageの安全性が保証されるわけではありません。

既知のVulnerability確認と、Packageそのものを信頼してよいかの確認は別です。

package-lock.jsonを勝手に削除していないか確認する

依存関係Errorを解消するため、

  • package-lock.jsonを削除してnpm installしてください

という修正を安易に採用すると、Dependency Treeが大きく変化する場合があります。

npm公式ドキュメントではpackage-lock.jsonは生成されたDependency Treeを正確に記述し、後のInstallでも同じTreeを再現するためのファイルとして説明されています。

CIでは、

npm ci

を使う方法もあります。

npm ciはLockfileとpackage.jsonが一致しなければErrorにし、Lockfileを勝手に更新しない動作です。

AIがDependency問題を解決するためにLockfileやVersion Constraintを大幅変更していないか、Diffを確認します。

「最新版へ全部更新」で直していないか確認する

AIへDependency Errorを直させると、

  • すべて最新版へ更新

という解決策になることがあります。

Security Patchを適用すること自体は重要ですが、大量のMajor Updateを一度に行うとBehavior Changeの原因を追跡しにくくなります。

Dependency変更もCode Reviewの対象にします。

  • どのPackageが変わったか
  • なぜ必要か
  • Major Versionが変わったか
  • Transitive Dependencyはどう変化したか
  • Lockfile Diffが異常に大きくないか

を確認し、必要な変更だけを採用します。

デバッグ用Endpointが残っていないか確認する

AIに障害調査を依頼すると、一時的に、

NG:環境変数を丸ごと返すデバッグ用Endpoint
app.get(
  "/debug/config",
  (_req, res) => {
    res.json(
      process.env,
    );
  },
);

のようなDebug Endpointを追加する場合があります。

開発中には便利でも、本番へ残せばSecretや内部構成が露出します。

また、

NG:認証なしで全ユーザー情報を返すEndpoint
app.get(
  "/debug/users",
  async (_req, res) => {
    res.json(
      await db.user.findMany(),
    );
  },
);

のような認証なしEndpointも危険です。

OWASPのSecurity Misconfigurationでは、不必要な機能、Test Framework、Account、Serviceなどを有効なままにすることもリスクとして挙げられています。

AIが追加したTemporary Codeは、Merge前に必ず確認します。

コメントアウトした認証処理が残っていないか確認する

エラー切り分け中に、

NG:認証Middlewareがコメントアウトされたまま残っている
// app.use(requireAuth);

と認証を一時停止することがあります。

AIエージェントに複数ファイルを編集させた場合、このような一時変更が別Fileへ残ることもあります。

レビューでは追加されたコードだけでなく、削除されたSecurity Codeも確認します。

Diffを見るときは、

  • 認証Middleware
  • Permission Check
  • Validation
  • Rate Limit
  • Timeout
  • Security Header

などが消えていないか注意します。

レスポンスへ必要以上のデータを返していないか確認する

AIに、

  • ユーザー情報を取得するAPIを作って

と依頼すると、

NG:Database Recordを丸ごと返している
const user =
  await db.user.findUnique({
    where: {
      id,
    },
  });

return res.json(user);

とDatabase Recordを丸ごと返すコードが生成されることがあります。

User Tableに、

  • passwordHash
  • resetToken
  • internalNote
  • billingId

などが追加されると、その値までAPI Responseへ出る可能性があります。

Response用の型を明示します。

OK:公開してよいフィールドだけを明示的に返す
return res.json({
  id:
    user.id,

  name:
    user.name,

  email:
    user.email,
});

Database ModelとPublic API Responseを同じ型として扱わないほうが安全です。

Mass Assignmentになっていないか確認する

更新APIにも似た問題があります。

NG:リクエストボディをそのままUPDATEに渡している(Mass Assignment)
await db.user.update({
  where: {
    id:
      req.user.id,
  },

  data:
    req.body,
});

一見すると非常に簡潔です。

しかしUser Modelに、

  • role
  • isAdmin
  • plan
  • creditBalance

などが存在すれば、ユーザーが本来変更できないFieldまで送信する可能性があります。

更新可能なFieldを明示します。

OK:更新可能なフィールドをスキーマで明示する
const schema =
  z.object({
    name:
      z.string()
        .min(1)
        .max(100),

    bio:
      z.string()
        .max(1000),
  });

const input =
  schema.parse(
    req.body,
  );

await db.user.update({
  where: {
    id:
      req.user.id,
  },

  data: input,
});

AIが生成した、

data: req.body

同様に確認したいパターン
Object.assign(
  user,
  req.body,
)

のような処理は重点的に確認します。

削除処理がGETになっていないか確認する

AIが簡単な管理ツールを作る際、

NG:状態を変更する処理がGETになっている
app.get(
  "/users/:id/delete",
  async (req, res) => {
    await deleteUser(
      req.params.id,
    );

    res.redirect(
      "/users",
    );
  },
);

のような実装を作ることがあります。

状態を変更する処理をGETへ置くのは避けます。

Crawler、Prefetch、リンク確認ツールなどがURLへアクセスしただけで副作用が発生する設計にしないことが重要です。

HTTP MethodだけでSecurityが保証されるわけではありませんが、APIの意味と副作用を一致させることは安全な設計の基本です。

削除や送信にHuman in the Loopが必要ではないか確認する

AI Agentがコードを直接実行するシステムでは、Code ReviewだけでなくTool Execution Boundaryも確認します。

たとえば、

  • delete_file
  • send_email
  • publish_post
  • create_order

をAIが自動実行できる場合です。

前の記事で解説したように、外部へ不可逆な副作用を与える処理は、実行直前にHuman Approvalを要求する設計を検討します。

「AIが正しいコードを生成したか」と「AIにその操作を自動実行させてよいか」は別のレビュー項目です。

AIのコメントを仕様だと思わない

AIはもっともらしいCommentも生成します。

NG:コメントと実装が一致していない
// 管理者のみ実行可能
async function deleteAllUsers() {
  return db.user.deleteMany();
}

しかしFunction内にはPermission Checkがありません。

コメントではなく実際のControl Flowを確認します。

同様に、

  • // SQL Injection対策済み
  • // Secure token
  • // Validate user input

と書かれていても、それだけで安全とは判断しません。

AI生成コードでは「説明と実装が一致しているか」を必ず確認します。

TODOがSecurity処理を先送りしていないか確認する

生成コードに、

  • // TODO: add authentication
  • // TODO: validate permissions
  • // TODO: rate limit

が残っている場合があります。

Prototypeでは意図的でも、そのままProductionへDeployされれば未実装のSecurity Boundaryになります。

レビュー時には、

  • TODO
  • FIXME
  • temporary
  • skip
  • disable
  • bypass

などを検索すると、暫定処理を発見しやすくなります。

Unit Testが成功するだけでは十分ではない

通常のAI生成Testは、

  • 正しい入力で200になる
  • Databaseへ保存できる
  • 期待したJSONになる

といった正常系へ偏りやすくなります。

Security Reviewでは失敗させるTestを追加します。

たとえば注文APIなら、

他人の注文が取得できないことを確認するテスト
it(
  "他人の注文は取得できない",
  async () => {
    const response =
      await request(app)
        .get(
          "/api/orders/other-user-order",
        )
        .set(
          "Authorization",
          userToken,
        );

    expect(
      response.status,
    ).toBe(404);
  },
);

入力検証なら、

limitの上限が守られることを確認するテスト
it(
  "limitの上限を超えられない",
  async () => {
    const response =
      await request(app)
        .get(
          "/api/users?limit=999999",
        );

    expect(
      response.body.items
        .length,
    ).toBeLessThanOrEqual(
      100,
    );
  },
);

を追加します。

NISTのSecure Software Development Frameworkでも、Human-readable CodeのReview/Analysisと、Executable CodeのTestingによって脆弱性を発見することがSecure Development Practiceとして挙げられています(SP 800-218のPW.7・PW.8)。

AI自身にレビューさせるだけで完了しない

生成したAIへ、

  • このコードにセキュリティ上の問題はありますか?

と再質問する方法は有効です。

別モデルにSecond Reviewさせる方法もあります。

しかし、AIレビューをHuman Review、Static Analysis、Dependency Audit、Testの代わりにはしません。

NIST SSDFも単一の手法ではなく、Code Review、Analysis、Executable TestingなどをSecure Development Processへ組み込む考え方を採っています。

AIはレビューを補助する道具として利用し、最終的なSecurity Boundaryはコード、設定、テスト、運用から確認します。

Diffが大きすぎる場合はいったん分割する

AIコーディングエージェントへ、

  • 認証を追加して
  • DBを変更して
  • APIも修正して
  • テストも直して

とまとめて依頼すると、大量のDiffが生成されます。

変更量が増えるほど、

  • 必要な変更
  • ついでに変更された部分
  • Security Checkの削除
  • Dependency変更

を見落としやすくなります。

Security-sensitiveな変更は小さく分割します。

たとえば、

  • Schema変更
  • 認証Middleware
  • Endpoint変更
  • Test追加

を別々のCommitまたはReview単位にすると、意図しない変更を見つけやすくなります。

「AIなら一度に大量修正できる」ことと、「一度に大量修正したほうが安全」は同じではありません。

最後にコードを上から読むのではなくデータの流れを追う

AI生成コードをレビューするときは、Fileを1行目から最後まで読むだけでは不十分です。

攻撃者が入力した値がどこへ流れるかを追います。

たとえば、

外部入力からSinkまでのデータの流れ
HTTP Request
  |
Validation
  |
Authorization
  |
Business Logic
  |
SQL
  |
External API
  |
File System
  |
Response
  |
Log

という流れです。

req.body.urlが最終的にfetch()へ入るならSSRFを確認します。

req.params.filereadFile()へ入るならPath Traversalを確認します。

req.body.queryがSQL文字列へ入るならInjectionを確認します。

req.body.commandexec()へ入るならCommand Injectionを確認します。

このように外部入力から危険なSinkまでを追うと、AI生成コードの見た目に惑わされにくくなります。

AI生成コードをレビューする実践的な順番

まずAuthenticationとAuthorizationを確認します。

次に外部入力がSQL、Shell、File Path、URL、HTMLへどう流れるかを追います。

その後、API KeyやPasswordなどSecretの扱い、Database Transaction、Concurrent Request、Retry、Idempotency、Timeout、Resource Limitを確認します。

さらにDependencyとLockfileの変更を確認し、最後にLog、Error Handling、Debug Code、Testを確認します。

この順番にすると、StyleやNamingを細かく直したあとで重大なSecurity Designの問題を発見し、全面的に書き直す状況を減らせます。

関連記事

AI生成コードのレビューに関するよくある質問

QAIが生成したコードはそのまま使ってはいけませんか
A用途によります。個人用Scriptや使い捨てPrototypeと、インターネットへ公開するAPIでは必要なReview Levelが違います。ユーザーデータ、認証、決済、File System、Shell、外部APIなどへアクセスするコードは、人間によるReviewとTestを行ってから採用するのが安全です。
QTypeScriptなら型があるので入力検証は不要ですか
A不要にはなりません。TypeScriptのStatic Typeは、HTTP Requestとして受け取ったJSONが実行時にその型であることを保証しません。外部入力にはRuntime Validationを行います。
QORMを使えばSQL Injection対策は完了しますか
A通常のParameterized APIを使うことでリスクを減らせますが、Raw SQLやUnsafe APIを利用すればInjectionの可能性があります。AIが生成したRaw Queryは特に確認します。
Qnpm auditが0件なら依存パッケージは安全ですか
A既知のVulnerabilityを調べる重要な手段ですが、安全性を完全に保証するものではありません。npm公式もnpm auditをDependencyの既知Security Vulnerabilityを確認する仕組みとして説明しています。Packageそのものの信頼性、Maintainer、Install Script、更新状況、不要なDependencyが増えていないかも別途確認します。
QAI生成コードを別のAIにレビューさせるのは有効ですか
A補助として有効です。ただし同じ設計上の前提を複数モデルが見落とす可能性があります。Static Analysis、Dependency Audit、Integration Test、権限Test、Human Reviewなどと組み合わせます。
Qとりあえず動くコードを作って後からSecurityを追加してもよいですか
APrototypeなら段階的に作る方法はあります。ただし認証、認可、データ分離、冪等性など、Architectureへ深く関係するSecurity要件は後付けすると大規模な変更になりやすいため、設計段階から考慮したほうが安全です。OWASP Top 10:2025にもInsecure Designが主要カテゴリとして含まれています。

まとめ

AIが生成したコードをレビューするときに、最も危険なのは、

  • 動いたから大丈夫

と判断することです。

SQL Injection、Broken Access Control、Path Traversal、Command Injection、SSRF、Hard-coded Credentialなどの脆弱性は、普通の入力で機能テストをしているだけでは発見できないことがあります。

2026年8月時点のOWASP Top 10:2025でも、Broken Access Control、Security Misconfiguration、Software Supply Chain Failures、Injection、Authentication Failures、Logging and Alerting Failures、Exceptional Conditionsの不適切な処理などが主要なApplication Security Riskとして扱われています。

AI生成コードでは、最初にAuthenticationとAuthorizationを確認します。

その後、外部入力がSQL、Shell Command、File Path、URL、HTMLなどの危険な処理へ到達していないかを追います。

さらにSecret、Dependency、Resource Limit、Timeout、Retry、Transaction、Concurrency、Idempotency、Logging、Error Handlingまで確認します。

そして正常系だけではなく、

  • 別ユーザーのIDに変更したらどうなるか
  • 巨大な値を入力したらどうなるか
  • 同じRequestを同時に送ったらどうなるか
  • 途中で外部APIが失敗したらどうなるか
  • 同じ処理がRetryされたらどうなるか

という条件でTestします。

NIST SSDFでも、Secure Software DevelopmentではCode Review/AnalysisとExecutable CodeのTestingによって脆弱性を発見・確認することが推奨されています。

AIコーディングではコードを書く時間を大幅に短縮できます。

その分、開発者が見るべき場所は「どう書くか」から「このコードをどこまで信用してよいか」へ変わります。

AI生成コードの最終レビューでは、コードが期待どおり動くことではなく、攻撃者が期待どおりに使ってくれなくても安全に動くことを確認するのが重要です。