示例说明
IN 子句是最容易破防的注入点:占位符数量未知时开发者常退化为字符串拼接,一次拼接就把用户输入升级为可执行 SQL。正确做法是按数组长度生成等量问号再拼进 SQL 骨架——拼的是结构性文本而非用户数据,值仍然逐个绑定。输入先做类型校验,恶意的引号与 OR 链只会被当作字面值匹配失败。
技术要点
- 占位符数量由数组长度生成
- 值全部走参数绑定通道
- 入口类型校验先行拒绝非法输入
代码解读
findEmails 校验全部 ID 为整数后构造 IN (?, ?, ?) 形式的骨架语句,all 展开数组绑定每个值;空数组短路返回避免生成非法的空 IN。try 块里的经典注入载荷被完整当作一个邮箱字符串参数查询,不产生任何语法效果。
使用建议
数组过大时分批查询,占位符数量有驱动上限。类型校验与参数化要同时做,二者互为纵深。ORM 的 whereIn 同样要确认其内部实现走绑定而非拼接。
代码示例
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)');
db.prepare('INSERT INTO users (email) VALUES (?)').run('[email protected]');
function findEmails(ids) {
if (ids.length === 0) return [];
if (ids.some((id) => !Number.isInteger(id))) {
throw new TypeError('ids must be integers');
}
const placeholders = ids.map(() => '?').join(', ');
const stmt = db.prepare(
'SELECT id, email FROM users WHERE id IN (' + placeholders + ')',
);
return stmt.all(...ids);
}
try {
// 恶意输入只是参数值,永远不会被解析成 SQL 语法
db.prepare('SELECT * FROM users WHERE email = ?').all("x' OR '1'='1");
} catch (err) {
console.log('injection attempt contained:', err instanceof Error);
}
const rows = findEmails([1, 2, 3]);
console.log('matched rows:', rows.length);
db.close();