直接用字符串拼接构造in条件是报表系统sql注入高发场景,必须将多选值转为数组并交由驱动做批量参数绑定,禁用字符串拼接,辅以白名单校验。

直接用字符串拼接构造 IN 条件,是报表系统里最典型的 SQL 注入高发场景。别信“前端做了下拉限制就安全”——只要后端没做参数化,攻击者就能绕过一切前端校验,把 ' OR 1=1 -- 塞进多选值里。
为什么 IN 拼接特别危险
多选下拉框传过来的通常是逗号分隔字符串(如 "1,2,5")或数组(如 ["1","2","5"]),开发者常直接拼进 SQL:"SELECT * FROM user WHERE id IN (" + ids + ")"。问题在于:
- 哪怕每个 ID 看起来是数字,攻击者也能提交
"1,2,5' OR '1'='1",最终变成IN (1,2,5' OR '1'='1) - 用
implode()或join()拼接时,如果没对每个元素单独参数化,整段字符串仍被当作文本解析 - ORM 的
whereIn()方法若传入的是未经校验的原始字符串,而非数组,照样中招
WHERE IN 必须用参数化数组,不能拼字符串
正确做法是把多选值转成数组,交给数据库驱动做批量参数绑定。不同语言示例:
PHP PDO(推荐):
$ids = $_GET['user_ids'] ?? [];<br>
if (!is_array($ids)) { throw new InvalidArgumentException(); }<br>
$placeholders = str_repeat('?,', count($ids) - 1) . '?';<br>
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");<br>
$stmt->execute($ids);
Python SQLAlchemy:
stmt = select(User).where(User.id.in_(user_ids))<br> # user_ids 是 Python list,SQLAlchemy 自动转为参数化占位符
Java MyBatis:
<select id="findByIds"><br>
SELECT * FROM user WHERE id IN <foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach><br></select><br>
// MyBatis 的 #{id} 是预编译参数,不是 ${id}
过滤和校验不能替代参数化
有人觉得“我加了 intval() 或正则校验就没事”,但这类措施极易漏掉边界情况:
-
intval("1' OR 1=1")返回1,看似安全,但若拼接逻辑写成"'$id'",单引号还在 - 允许字母的业务字段(如部门编码
"HR-001")无法用intval(),正则稍一宽松就放行恶意字符 - 空数组、null、超长数组等异常输入没处理,可能触发 SQL 语法错误或 DoS
真正有效的前置校验只有两条:
– 检查是否为数组且非空
– 对每个元素做类型/格式白名单判断(如只允许数字或特定长度的字母数字组合)
但即便如此,仍必须走参数化,校验只是辅助。
报表导出类接口最容易被忽略
报表系统常有“导出全部”“按条件导出”功能,后端为图省事,把用户传来的 WHERE 条件字符串直接拼进 SQL。这类接口往往权限高、数据量大、日志少,是攻击者的首选目标。
修复要点:
– 禁止接收任意 SQL 片段,改用结构化查询参数(如 { "status": ["active","pending"], "date_range": ["2024-01-01", "2024-12-31"] })
– 所有字段名、操作符(=、LIKE、BETWEEN)必须硬编码或从白名单映射
– ORDER BY 字段必须限定在预设列名内,禁止传 "id; DROP TABLE users"
最麻烦的不是写代码,而是清理历史遗留的动态拼接逻辑——那些藏在报表模板渲染层、Excel 导出工具类里的裸字符串拼接,比主业务代码更难定位和改造。











