SQL 注入是 Web 應用最頂級的安全風險之一。在 Node.js 使用 mysqljs/mysql 驅動操作 MySQL 時,**絕對禁止字符串拼接 SQL**,必須使用驅動內建的參數化/轉義機制。本文系統總結四種核心防注入姿勢,附代碼示例與轉義規則詳解。
驅動內部會將參數進行**上下文感知的轉義**,生成安全的 SQL 文本發送給服務端,徹底切斷注入路徑。
escape() 手動轉義參數適用於動態構建 SQL 片段、IN 列表等場景。
const mysql = require('mysql');
const connection = mysql.createConnection({...});
const userId = 1;
const name = "test'; DROP TABLE users; --"; // 惡意輸入
// 三種調用方式等價
const sql1 = 'SELECT * FROM users WHERE id = ' + connection.escape(userId) + ' AND name = ' + connection.escape(name);
const sql2 = 'SELECT * FROM users WHERE id = ' + mysql.escape(userId) + ' AND name = ' + mysql.escape(name);
const sql3 = 'SELECT * FROM users WHERE id = ' + pool.escape(userId) + ' AND name = ' + pool.escape(name);
connection.query(sql1, (err, results) => { ... });
// 生成安全 SQL: SELECT * FROM users WHERE id = 1 AND name = 'test\';\ DROP TABLE users; --'
escape() 轉義規則詳解:| 輸入類型 | 轉義結果 |
|---|---|
| Number | 原樣輸出 (不加引號) |
| Boolean | true / false |
| Date | 'YYYY-MM-DD HH:mm:ss' (MySQL DATETIME 格式) |
| Buffer | 十六進制字面量 X'0fa5' |
| String | 安全轉義:單引號→\'、反斜線→\\、控制字符轉義 |
| Array | 轉為列表 'a', 'b' (適合 IN (...)) |
| 多維數組 | 轉為分組列表 ('a', 'b'), ('c', 'd') (適合 VALUES (...), (...)) |
| Object | 轉為 key=value 鍵值對 (適合 SET/WHERE),嵌套對象轉為 JSON 字符串 |
| undefined / null | NULL |
| NaN / Infinity | **觸發 MySQL 錯誤** (不支援) |
? 佔位符 (推薦標準做法)connection.query() 內部自動調用 escape(),代碼最簡潔、可讀性最佳。
const userId = 1;
const name = 'test';
// 陣列佔位符 (順序對應)
connection.query('SELECT * FROM users WHERE id = ? AND name = ?', [userId, name], (err, results) => { ... });
// 物件佔位符 (鍵值對自動展開為 `key = ?`,極其適合 INSERT/UPDATE)
const post = { userId: 1, name: 'test', email: 'test@example.com' };
connection.query('INSERT INTO posts SET ?', post, (err, result) => { ... });
// 生成: INSERT INTO posts SET `userId` = 1, `name` = 'test', `email` = 'test@example.com'
// WHERE 子句物件用法
connection.query('SELECT * FROM users WHERE ?', { status: 1, role: 'admin' }, (err, rows) => { ... });
// 生成: SELECT * FROM users WHERE `status` = 1 AND `role` = 'admin'
escapeId() 轉義標識符 (表名/列名/庫名)**參數化查詢無法處理標識符** (如動態表名、動態排序列)。若標識符來自用戶輸入,必須用 escapeId() 包裹反引號並轉義內部反引號。
const sortColumn = 'created_at'; // 或來自用戶輸入 'created_at; DROP TABLE users; --'
const direction = 'DESC';
// 錯誤做法: sql = 'SELECT * FROM posts ORDER BY ' + sortColumn + ' ' + direction;
// 正確做法:
const sql = 'SELECT * FROM posts ORDER BY ' + connection.escapeId(sortColumn) + ' ' + connection.escape(direction);
// 生成: SELECT * FROM posts ORDER BY `created_at` 'DESC' (direction 仍需 escape 為字符串)
// 三種調用方式
mysql.escapeId(identifier);
connection.escapeId(identifier);
pool.escapeId(identifier);
mysql.format() 準備查詢 (高級模板)類似 printf 風格,支援 ?? (標識符佔位符) 和 ? (值佔位符),自動選擇合適轉義方法。
const table = 'users';
const column = 'id';
const value = 1;
const sql = "SELECT * FROM ?? WHERE ?? = ?";
const inserts = [table, column, value];
const formattedSql = mysql.format(sql, inserts);
// 生成: SELECT * FROM `users` WHERE `id` = 1
// 直接執行
connection.query(formattedSql, (err, results) => { ... });
// 或結合 query 直接使用 (內部會調用 format)
connection.query(sql, inserts, (err, results) => { ... });
escape() 拼接,或使用查詢構建器 (Knex.js, TypeORM, Sequelize) 進一步抽象。VALUES (?,?), (?,?)。connection.query('SELECT * FROM ' + userInputTable)、connection.query('SELECT * FROM users WHERE name = \'' + userInput + '\'')。mysql.createPool() 復用連接,提升性能並統一配置。@types/mysql),編譯期捕獲參數類型錯誤。Node.js MySQL 防注入的核心是 **「數據與代碼分離」**。mysqljs/mysql 提供了完備的轉義工具鏈:
? 佔位符 (首選) / escape() / format()escapeId() / format() 的 ??將這四種姿勢內化為肌肉記憶,配合 Code Review 機制攔截任何字符串拼接 SQL,構建起堅實的應用層安全防線。