flawopen.com/sql-injection/Javascript
●
CWE-89 · 緊急
セキュリティ研究 · FlawOpen
JavaScript の SQL インジェクション(mysql2・pg・Prisma・Knex)
pool.query() の中でテンプレートリテラルや + を使うと、リクエストパラメータが SQL を書き換え、全行が返されます(CWE-89)。mysql2・node-postgres・Prisma・Knex での安全な呼び出し方、${} が Prisma のあるメソッドでは安全なのに隣のメソッドでは危険な理由、orderByRaw を使わない並べ替え方を解説します。[1], [2], [3], [4], [5], [6]
💡 わかりやすい解説 (ELI5)
荷物の仕分けセンターを想像してください。荷物が届く前に配送ルートを印刷して固定します。ラベルの名前は用意された欄に入り、ルートに新しい立ち寄り先を追加できません。SQL パラメーターも同じです。mysql2 execute(sql, [userId]) と pg query(sql, [status]) は値をバインドし、Prisma のタグ付き $queryRaw は各 ${} の位置をバインドします。先にテンプレートリテラルや + で SQL を組み立てたり、$queryRawUnsafe を使ったりすると、利用者の文字列がルート自体に書き込まれます。Knex orderByRaw もソート指定で同じ問題が起きるため、要求キーを固定列に対応づけて orderBy を呼びます。[1], [2], [3], [4], [5], [6]
主要な概念と専門用語
Template literals in query()${userId}を含むバッククォート文字列は、ドライバーが呼ばれる前に JavaScript が評価します。ドライバーが受け取るのは、値がすでに埋め込まれた完成済みの SQL です。見た目はプレースホルダーですが、何もバインドしません。[7], [1], [6]mysql2 execute() vs query()execute(sql, values)はサーバー側のプリペアドステートメントを使います。query(sql, values)はクライアント側で値をエスケープし、?マーカーに差し込みます。値が配列にあればどちらも安全ですが、SQL 文字列を+やテンプレートリテラルで組み立てた場合はどちらも役に立ちません。mysql2 はmultipleStatementsをデフォルトで無効にしています。[3], [6], [9]pg query(text, values)- node-postgres は
valuesをtextとは別に送るため、$1を埋めるのは PostgreSQL です。値がない場合 pg は単純クエリプロトコルを使い、これはセミコロン区切りの複数の文も 1 回の呼び出しで受け付けます。[2], [6]
フレームワーク対応
- このガイドで説明する弱点の CWE 参照: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
ステップ・バイ・ステップの攻撃フロー
ステップ 1
ステップ 2
ステップ 3
ソースコード比較:脆弱 vs 堅牢化
脆弱な実装
// users.js: 以下のクエリはどれもリクエストのデータから SQL を組み立てている
// mysql2: ドライバーが呼ばれる前にテンプレートリテラルが評価される
export async function getUser(pool, userId) {
const [rows] = await pool.query(`SELECT id, email FROM users WHERE id = ${userId}`);
return rows[0];
}
// pg: 連結しており、バインドする値の配列もない
export async function findOrders(pg, status) {
const { rows } = await pg.query("SELECT id FROM orders WHERE status = '" + status + "'");
return rows;
}
// Prisma: Unsafe 版は完成した文字列を受け取る
export function findByEmail(prisma, email) {
return prisma.$queryRawUnsafe(`SELECT id FROM "User" WHERE email = '${email}'`);
}
// Knex: orderByRaw はリクエストの値を生の SQL として挿入する
export function listUsers(knex, sortColumn) {
return knex('users').select('id', 'email').orderByRaw(sortColumn);
}
堅牢化されたセキュアパッチ
// users.js: 値は値の配列で渡し、カラムは許可リストから取る
const SORTABLE = new Map([['id', 'id'], ['email', 'email'], ['created', 'created_at']]);
// mysql2: サーバー側プリペアドステートメント。値は ? にバインド
export async function getUser(pool, userId) {
const [rows] = await pool.execute('SELECT id, email FROM users WHERE id = ?', [userId]);
return rows[0];
}
// pg: PostgreSQL が値の配列から $1 を埋める
export async function findOrders(pg, status) {
const { rows } = await pg.query('SELECT id FROM orders WHERE status = $1', [status]);
return rows;
}
// Prisma: タグ付きテンプレート。各 ${} がバインドパラメータになる
export function findByEmail(prisma, email) {
return prisma.$queryRaw`SELECT id FROM "User" WHERE email = ${email}`;
}
// Knex: orderBy が許可済みのカラムを識別子としてクォートする
export function listUsers(knex, sortKey) {
const column = SORTABLE.get(sortKey) ?? 'id';
return knex('users').select('id', 'email').orderBy(column);
}
エンジニアリング&システム堅牢化チェックリスト
- ✓
$queryRawUnsafe、$executeRawUnsafe、補間を使うwhereRaw/orderByRaw、そしてquery()/execute()の第 1 引数でのテンプレートリテラルや+を、ESLint のno-restricted-syntaxルールか CI の Semgrep で禁止する。[1], [3], [2], [4], [5], [6] - ✓すべての値はドライバーの値の配列(mysql2 は
?、pg は$1)か、Prisma のタグ付き$queryRawの${}で渡す。[3], [2], [4], [6] - ✓ルートでスキーマ(zod、Joi)により入力を検証する。ID は数値に変換し、未知のキーは拒否し、文字列を期待する箇所で配列を受け付けない。[8], [6]
- ✓ソートキーは
Mapの許可リストで対応付け、その結果を Knex のorderBy()か Prisma のorderByに渡す。req.queryを生の ORDER BY に渡さない。[5], [4], [6] - ✓mysql2 の
multipleStatementsは無効のままにし、DDL 権限のないデータベースユーザーで接続する。[3], [9], [6]
参考資料
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- node-postgres — Parameterized query
- mysql2 — Using prepared statements
- Prisma — Raw queries and SQL injection
- Knex.js — Raw parameter binding
- OWASP SQL Injection Prevention Cheat Sheet
- MDN — Template literals (JavaScript)
- Express 4 API — req.query
- MySQL C API — Multiple statement execution support