SQL注入防护需结合预处理语句与白名单校验、映射表、动态占位符控制;脱敏应字段级管控、代理层优先、保留原始值检索、JSON路径专项处理,并覆盖组合场景与导出接口。

SQL注入防护不能只靠预处理语句
预处理语句(如 PreparedStatement)确实能拦住绝大多数拼接式注入,但一旦业务里出现动态表名、列名、排序字段或 IN 子句中不定长参数,它就失效了——这些地方根本没法参数化。这时候如果还硬套 PreparedStatement,反而会让人误以为“已经安全了”,更危险。
- 动态表名/列名必须白名单校验:只允许出现在预设集合里的字符串,比如
["users", "orders"],拒绝任何含点号、括号、空格或 SQL 关键字的输入 -
ORDER BY后的字段名别用用户输入直接拼,改用映射表:sort_field_map = {"name": "real_name", "time": "created_at"},查不到就默认不排序 -
IN (?, ?, ?)的问号个数必须由服务端控制,不能把用户传来的数组长度直接拿来生成占位符——得先限制最大长度(比如 ≤ 100),再构造固定数量的?
数据脱敏不是加个 SELECT CONCAT('***', SUBSTR(phone, -4)) 就完事
这种写法在应用层看似简单,但会破坏索引、拖慢查询,且脱敏逻辑散落在各处,后续审计或合规检查时根本理不清哪些字段被脱敏、哪些没脱敏、规则是否统一。
- 优先在数据库代理层或中间件做脱敏(如 ShardingSphere 的
EncryptRuleConfiguration),让应用无感,也避免重复实现 - 若必须在 SQL 层做,用
CASE WHEN+ 字段元数据判断角色,而不是每个查询都手写脱敏表达式;例如:对role = 'guest'的请求,自动将phone替换为脱敏函数调用 - 注意
LIKE查询和脱敏冲突:脱敏后的手机号是***1234,但用户搜%1234就查不到——得保留原始值用于检索,仅展示时脱敏
脱敏规则和权限控制必须绑定到字段级,而非表级
一个 users 表里,id 和 nickname 可以明文,id_card 要加密存储,phone 要展示脱敏但导出时可解密——表级开关根本分不清这种差异,一开全开、一关全关,要么泄露,要么误杀。
- 建字段注释或扩展元数据表,记录每列的
sensitive_level(如"PII"、"PCI")、mask_rule(如"hide_middle_4")、export_allowed(布尔值) - ORM 层(如 MyBatis Plus)拦截
ResultMap解析,在反射赋值前根据当前用户权限和字段规则决定是否替换值 - 警惕 JSON 字段:
extra_info这种 text 类型里可能嵌套身份证号,常规字段规则不生效,得额外配置 JSONPath 规则(如$.user.id_card)
测试时最容易漏掉的是「组合场景」
单独测预处理语句通过、单独测脱敏函数返回正确,不代表上线就安全。真实攻击者会把注入和脱敏漏洞串起来打——比如利用日志接口回显未脱敏的报错信息,再结合宽字节注入绕过过滤。
- 必须测
UNION SELECT是否能从脱敏字段里捞出原始值(检查是否用了CONVERT或CAST导致隐式类型转换绕过) - 检查错误页面是否暴露堆栈或 SQL 片段(哪怕只是
MySQLSyntaxErrorException这类标准异常,也可能被用来盲注) - 导出功能常被忽略:API 返回脱敏数据,但 Excel 导出接口却直连 DAO 查原始值,且没鉴权——这类接口要单独加
@PreAuthorize("hasRole('ADMIN_EXPORT')")级别的控制
脱敏策略真正难的不是写几行掩码逻辑,而是让规则能随权限变、随字段走、随上下文生效,同时不拖垮性能、不干扰排查。很多团队卡在“规则配不全”和“配了不敢信”之间来回横跳。

















