动态WHERE条件必须参数化并显式处理空值,禁用字符串拼接;ORDER BY/GROUP BY等非参数化位置须白名单校验;IN查询需整体绑定;权限最小化且日志不记录原始参数。

动态 WHERE 条件必须用参数化 + 显式空值逻辑,不能靠字符串拼接
报表查询常支持“品牌可选、价格区间可选、状态可选”这类组合,很多人误以为只要用了 PreparedStatement 就安全了,其实不然——若在代码里手动拼 " AND status = " + status,哪怕后续传给 executeQuery(),也已埋下注入漏洞。
关键问题在于:空值或未传字段会破坏 SQL 结构,而拼接本身就把用户输入当成了代码的一部分。
- 错误写法:
" AND brand = '" + brand + "'"——brand为空时生成AND brand = '',语义错;若含' OR 1=1 --则直接穿透 - MySQL 推荐写法:
AND (COALESCE(?, '') = '' OR brand = ?),两个?分别绑定空检查值和实际值 - PostgreSQL 更简洁:
AND ($1 IS NULL OR brand = $1),但需确认 JDBC 驱动支持位置参数与NULL兼容 - MyBatis-Plus 场景下,优先用
queryWrapper.like("brand", brand).eq("status", status)链式调用,内部已做参数封装;避免手写@Select+ 字符串插值
ORDER BY / GROUP BY / LIMIT 等非参数化位置必须白名单校验
ORDER BY、GROUP BY、表名、列名这些地方数据库根本不允许 ? 占位,硬塞会报语法错误。很多开发者用 ${sortField} 或 ORDER BY ? 试图“绕过”,结果全军覆没。
真实风险点在于:攻击者控制排序字段,就能把 sortField=price; DROP TABLE users-- 这类 payload 注入进去。
- 正确做法:定义硬编码映射表,如
Map.of("price", "price ASC", "time", "create_time DESC"),再从用户输入中查出对应 SQL 片段 -
LIMIT ?, ?是合法的,但OFFSET必须校验为非负整数,防止传-1导致全表扫描 - MyBatis 中
GROUP BY ${groupField}必须配合正则校验:^[a-zA-Z0-9_]+$,拒绝任何含空格、逗号、分号的输入 - 报表工具(如积木报表)若开放
{xxx}参数替换,务必禁用除预设字段外的所有变量名,不接受任意 key
多值 IN 查询要整体绑定,禁止循环拼字符串
前端传 {"brands": ["苹果", "华为"], "statuses": [1, 2]} 时,常见错误是 for 循环里反复调用 in("brand", brand) 或拼 "'苹果','华为'",即使用了 like() 方法,底层若未走预编译,照样危险。
本质问题是:IN 子句的多个值是一个逻辑单元,不能拆成多个独立参数绑定,也不能让驱动去猜你有几个值。
- MyBatis-Plus 推荐:
wrapper.in("brand", brandList)—— 内部自动展开为IN (?, ?, ?)并绑定全部值 - 若用原生 SQL,可用
apply("brand IN ({0})", brandList)(需 MyBatis-Plus ≥ 3.4.3),但注意版本兼容性 - 绝对禁止:
for (String b : brands) { sql += "'" + b + "',"; }—— 单引号逃逸、Unicode 编码绕过都可能生效 - 更稳妥方案:对数组参数先做长度限制(如最多 100 项)+ 值类型强校验(只允许字符串/数字),再进 IN
报表场景下最易被忽略的权限与日志盲区
即使所有 WHERE、ORDER BY 都做了参数化和白名单,报表仍可能因权限配置不当或日志暴露而泄露数据。这是生产环境最常踩的坑。
- 数据库账号必须最小权限:仅授予
SELECT权限,禁用UNION、INTO OUTFILE、存储过程调用等高危能力 - 报表 SQL 执行日志不能记录原始参数值,尤其避免将
username=admin' OR '1'='1这类内容写入磁盘 - 前端传参字段名必须与后端白名单严格一致,比如只允许
startTime/endTime,但若接口同时接收start_time,就可能绕过校验 - 所有动态拼接逻辑必须有单元测试覆盖边界 case:空数组、null 字符串、超长字段、SQL 关键字作为值(如 brand="SELECT")











