SQL注入防护需多层防御:预处理语句防拼接注入但不防动态表名列名;白名单校验动态结构、最小权限原则、禁查INFORMATION_SCHEMA、禁用危险存储过程与视图定义泄露、管控ORM透明性。

SQL注入防护不能只靠预处理语句
预处理语句(PreparedStatement)能防绝大多数拼接式注入,但挡不住权限绕过、列名/表名动态拼接、函数名注入等场景。比如用 ORDER BY ? 传参,数据库会直接报错——因为排序字段不支持参数化;又比如用 SELECT * FROM ? 拼接表名,PreparedStatement 根本不处理这个位置。
真正要保表结构,得在应用层和数据库层各守一关:
- 所有动态表名、列名、排序字段、GROUP BY 字段,必须走白名单校验,不能用正则模糊匹配,得是精确枚举(如
["user", "order", "product"]) - 数据库账号严格按最小权限原则分配:普通接口账号禁止执行
SHOW TABLES、DESCRIBE、INFORMATION_SCHEMA查询 - 禁用存储过程中拼接 SQL 的
CONCAT()+EXECUTE组合,改用条件分支或拆成多个固定语句
INFORMATION_SCHEMA 是攻击者的第一张地图
只要账号有查询权限,INFORMATION_SCHEMA.TABLES 和 INFORMATION_SCHEMA.COLUMNS 就等于把整个库结构明文交出去。很多团队开了只读账号就以为安全了,其实只要能查 schema,就能自动推导出敏感字段名、外键关系,甚至猜出加密字段的业务含义。
关键动作不是“限制 SELECT”,而是从源头掐断访问路径:
- MySQL 8.0+ 可用
REVOKE SELECT ON INFORMATION_SCHEMA.* FROM 'app_user'@'%'显式回收(注意:该权限默认对所有用户开放) - PostgreSQL 需
REVOKE SELECT ON ALL TABLES IN SCHEMA information_schema FROM app_user,且要在pg_catalog同样操作 - 别依赖中间件屏蔽关键词(如过滤
INFORMATION_SCHEMA),SQL 注入能用大小写、注释、编码绕过这类规则
触发器和视图也能暴露结构细节
有些团队用视图封装敏感字段,却忘了视图定义本身可被查:SELECT VIEW_DEFINITION FROM INFORMATION_SCHEMA.VIEWS。同样,触发器逻辑里若包含 INSERT INTO audit_log (table_name, old_data) 这类硬编码表名,也会在 INFORMATION_SCHEMA.TRIGGERS 中泄露。
防御要点很实在:
- 生产环境禁用
SHOW CREATE VIEW和SHOW CREATE TRIGGER权限,哪怕账号只有 SELECT - 审计日志类表名不要带业务含义(避免用
user_audit_log,改用evt_log_01这类无提示名称) - 触发器里涉及表名的逻辑,改用存储过程封装,并将过程权限设为
DEFINER,调用者无法看到内部 SQL
ORM 框架的“自动映射”反而是个风险点
像 Hibernate 的 @Table(name = "user") 或 Django 的 class Meta: db_table = "user",一旦开启调试模式或错误回显,堆栈里就可能带出真实表名。更危险的是 JPA 的 findByUserNameContaining 这类方法名解析,底层生成的 SQL 若含 LIKE '%?%',配合错误信息里的字段类型提示,攻击者能反推出列名和索引结构。
实际要卡住的不是 ORM 本身,而是它的“透明性”:
- 关闭所有框架级 SQL 日志(如
spring.jpa.show-sql=true必须为false) - 禁止将数据库错误原样返回前端,
org.hibernate.exception.SQLGrammarException这类异常必须统一转成 500 并抹掉消息体 - 表名/列名映射配置不要写死字符串,用常量类集中管理,方便后续加混淆或替换逻辑
表结构不是藏起来就安全,是让攻击者即使拿到部分信息,也拼不出完整拓扑。最易被忽略的,是开发环境和测试库的权限配置——它们往往比生产库宽松得多,而攻击链经常从这里起手。










