← 中文资源

简体中文版本

SQLite 参数化 IN 子句安全

动态 IN 子句用生成的占位符拼接而绝不内插用户值:每个值都作为绑定参数传递,注入字符串只会被当成普通数据处理。

阅读英文原文

示例说明

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();