必须动态生成等量参数占位符并绑定值,严禁字符串拼接in列表;需显式校验空数组、非数组、超长及非法格式数据,并对表名、字段名、排序等sql结构项实施白名单控制。

批量查询中的 SQL 注入漏洞,绝大多数源于对 IN 子句的字符串拼接——这不是“可能不安全”,而是“必然不安全”。只要用了 implode()、join()、f-string 或 + 把用户传来的多选值直接塞进 SQL,就已失守。
WHERE IN 必须用参数化数组,不能拼字符串
多选下拉框通常提交类似 ["1", "5", "12"] 或逗号分隔字符串 "1,5,12"。开发者容易写成:
SELECT * FROM user WHERE id IN (" . $_GET['ids'] . ") —— 这是典型高危写法。
攻击者传 ids=1,2,5%27%20OR%201%3D1--(即 1,2,5' OR 1=1--),最终 SQL 变成:IN (1,2,5' OR 1=1--),注释掉后续条件,直接越权读取全表。
- PHP PDO 正确做法:确保
$_GET['ids']是数组,用str_repeat('?,', count($ids)-1) . '?'生成占位符,再$stmt->execute($ids) - Python SQLAlchemy:必须传
list给.in_(),如User.id.in_(user_ids);若传字符串"1,2,3",SQLAlchemy 不会拆解,会当单个值查 - Java MyBatis:
<foreach></foreach>中的#{id}才安全;若误写成${id},仍会拼接字符串
空数组、非数组、超长数组必须显式拦截
参数化本身不处理边界异常。以下情况若无校验,会直接报错或被利用:
-
$_GET['ids']是null或字符串"1,2,3",未is_array()判断就进count(),导致 Warning 或空占位符IN ()语法错误 - 数组长度超 1000 项(如导出全量数据),某些数据库(如 MySQL)有
max_allowed_packet或预编译参数上限,可能触发连接中断或 DoS - 元素含空字符串
""或非数字字符串(如"HR-001"),若业务允许,需白名单校验格式;若只接受数字,应逐项filter_var($id, FILTER_VALIDATE_INT)并丢弃失败项
text()、raw()、extra() 等“看似 ORM”接口照样中招
ORM 框架仅对标准查询方法(如 filter()、query().where())自动参数化。以下全是高危区:
- Flask-SQLAlchemy 的
db.session.execute(text("SELECT ... WHERE id IN ({})".format(ids_str)))——text()内部拼接即沦陷 - Django 的
extra(where=["id IN ({})".format(ids_str)])或raw("SELECT ... WHERE name = '{}'".format(name)) - SQLAlchemy 的
select(...).where(text("status = '{}'".format(status)))—— 占位符必须在text()字符串内,且所有变量走第二参数传入
真正安全的写法只有:text("SELECT ... WHERE id IN :ids") + {"ids": [1,5,12]}(注意:psycopg2 支持命名数组绑定,SQLite 不支持,需按驱动能力选型)。
表名、字段名、ORDER BY 不能参数化,必须白名单
批量查询常伴随动态排序或分表逻辑,比如 ORDER BY {sort_field} 或 FROM {table_suffix}。这些属于 SQL 语法结构,数据库引擎不允许参数化。
- 排序字段:只允许从固定集合取值,如
allowed_sort = ["created_at", "name", "score"],用in判断,否则 fallback 到默认字段 - 分表名:用映射字典控制,如
{"2024": "order_2024", "2025": "order_2025"},用户传year=2024才取对应表名,禁止直接拼接 - LIMIT 数量:强制
int()转换并限范围,如max(1, min(500, int(limit))),避免传999999999导致内存溢出
最易被忽略的是:白名单校验必须在参数化之前做,且不能依赖前端传来的任意字符串做键名索引——那等于把白名单字典本身暴露给了攻击面。











