FlawOpen

flawopen.com/SQL Injection/数値 ID

数値 ID でも文字列フォーマットが危険なのはなぜですか?

リファレンスページ
結論

危険性は値の実際の意味ではなく、クエリを文字列の連結で作ることにあります。「常に数値」というのはデータへの想定であり、コードが強制する保証ではありません。

想定が崩れる場面

HTTP パラメーターは常に文字列として届く

URL のクエリパラメーターやフォーム値は、DB 列が期待する型にかかわらず、通信上はテキストです。型を明示的に検証しない限り、コードに届く前に id=1 OR 1=1 のような値を拒否する仕組みはありません。

将来のリファクタリングで値の出所が変わることがある

ID が常に信頼できる内部ソース由来なので今は安全なコードでも、新しい呼び出し元がユーザー値を同じ関数に渡せば危険になります。脆弱性はその変更を待って潜在していたのです。

型変換で実際の値が隠れることがある

特に動的型付け言語では、ログ上は数値に見える値でも追加文字を含む文字列の場合があります。明示的に検証しない限り、期待する型と実際の型は一致するとは限りません。

型を検証した後もクエリをパラメーター化する

アプリケーションが期待する形式に ID を検証してから、データベースドライバーのパラメーター API に渡してください。検証は入力契約を守り、パラメーター化は入力が SQL 構文として解釈されるのを防ぎます。プロトコル上のドライバーの動作は異なる場合があるため、すべてのドライバーが同じ方法で値を送るとは限りません。PostgreSQL のパラメーターはテーブル名や列名を表せません。動的なクエリ構造はアプリケーション内の固定許可リストから選んでください。

数値をバインドし、意味を検証する

アプリケーションがその型を必要とする場合は、ID が正の安全な整数であることを検証したうえで、クエリ値としてバインドしてください。検証はアプリケーションの入力契約を守り、パラメーター化は入力が SQL 構文になるのを防ぎます。PostgreSQL のパラメーターはテーブル名や列名の代わりには使えないため、動的なクエリ構造はアプリケーション内の固定許可リストから選択してください。

危険 — ID を SQL に埋め込む
const rawId = new URL(request.url).searchParams.get("id");
const result = await pool.query(
  `SELECT id FROM users WHERE id = ${rawId}`
);
安全 — ID を検証して値としてバインドする
const rawId = new URL(request.url).searchParams.get("id");
if (rawId === null || !/^[0-9]+$/.test(rawId)) {
  return new Response("Invalid id", { status: 400 });
}
const id = Number(rawId);
if (!Number.isSafeInteger(id) || id < 1) {
  return new Response("Invalid id", { status: 400 });
}
const result = await pool.query(
  "SELECT id FROM users WHERE id = $1",
  Array.of(id)
);

参考資料

関連項目 SQL インジェクション(CWE-89)の仕組みと各言語での防ぎ方 — flawopen.com パラメータ化クエリとは何か Prisma $queryRaw は SQL Injection に安全? Railsのwhere()文字列補間は安全? Sequelize query() は SQL Injection に安全? SQLAlchemy text() は SQL Injection に安全? sqlx のコンパイル時クエリ検査は SQL インジェクションを防ぎますか?