对象关系映射框架本意是屏蔽SQL细节、降低出错概率,但在实际项目中仍然频繁出现借助ORM发起的SQL注入。根本原因往往不在于框架本身有缺陷,而是开发者在追求灵活查询时,绕开了框架的参数绑定机制,使用其暴露的原生查询能力直接拼接字符串。即便主流ORM都提供了预编译与占位符功能,一旦落入手工拼语句的陷阱,数据库执行层就无法区分代码与数据。

原生查询接口为何成为注入突破口
几乎所有ORM都会提供一种“ escape hatch”式的原生查询入口,例如Hibernate的createSQLQuery、Entity Framework的FromSqlRaw、Django的raw。这些接口的存在是为了弥补框架DSL在复杂报表、数据库特有函数上的不足。问题在于,它们通常既支持参数绑定,也允许传入一段完整SQL字符串。当开发者图省事将前端参数直接并入字符串,就等同于在JDBC的Statement上做字符串拼接。
从数据库协议角度看,无论SQL是由ORM生成还是手写,最终都以文本形式发往服务端解析。若文本中混入了用户输入且未用占位符隔离,解析器便会将数据当作命令的一部分。比如查询条件中原本应是where name = '张三',攻击者传入张三' or '1'='1,拼接后变成两条逻辑条件的或关系,返回了全表数据。ORM在此场景里只是传声筒,并没有任何自动清洗能力。
常见框架的危险写法示例
下面以Hibernate为例,展示一段容易产生注入的代码。注意其中使用字符串拼接而非参数绑定:
// 错误示例:直接拼接用户输入
String userInput = request.getParameter("name");
String sql = "select * from users where name = '" + userInput + "'";
SQLQuery query = session.createSQLQuery(sql);
List result = query.list();
上述代码在userInput包含单引号与逻辑运算符时会改变SQL语义。相对安全的写法应当使用命名参数,让框架在底层调用PreparedStatement:
// 正确示例:使用命名参数绑定
String sql = "select * from users where name = :name";
SQLQuery query = session.createSQLQuery(sql);
query.setParameter("name", userInput);
List result = query.list();
MyBatis中类似的风险来自${}与#{}的差异。前者做字符串替换,后者生成占位符。若排序字段使用order by ${column}且column来自前端,就构成了注入点。开发时应尽量用#{},对必须动态传入的标识符采用服务端白名单校验。
仅禁用原生接口并不能根除风险
有些团队规定“禁止调用任何原生查询接口”,希望借此杜绝注入。这种做法在理论上减少了危险入口,但实际业务里往往难以执行。报表统计、地理空间查询、窗口函数等需求,ORM的封装要么不支持要么性能极差,最终开发人员会偷偷用字符串拼接的存储过程或外部SQL文件绕过规范。
更关键的是,即便只使用ORM的封装方法,如果误用API仍可能中招。例如Hibernate的Restrictions.sqlRestriction允许写片段SQL,若里面拼了变量同样危险;JPA的CriteriaBuilder虽类型安全,但一旦用function嵌入原生函数并拼参数,风险回归。因此安全的核心不是避开某个接口,而是建立“任何外部输入都必须通过绑定或校验”的编码习惯。
建立参数化与校验的双重机制
对于必须使用原生SQL的地方,建议封装统一的执行器,内部强制使用占位符,并对非值类型的输入(如表名、列名)做枚举白名单。如下简表列出不同输入类型的处理方式:
| 输入用途 | 处理方式 | 说明 |
|---|---|---|
| 查询值(where条件) | 参数绑定 | 使用?或命名参数,禁止拼接 |
| 排序字段 | 白名单映射 | 前端传数字编号,后端映射为真实列名 |
| 表名 | 配置常量 | 不允许用户直接指定,从服务端配置读取 |
在代码评审环节,应当把“是否出现字符串加号连接SQL”作为红线规则。静态扫描工具也可以识别createSQLQuery(后面跟随变量拼接的模式并报警。只有将框架能力、规范约束与工具检查结合,才能在保留原生查询灵活性的同时控制注入风险。
从架构层面降低出错概率
除了编码习惯,架构设计也能减少人为失误。比如将复杂查询统一收口到只读服务,由专门的数据访问层提供方法签名,业务代码只能传参不能写SQL。再如采用GraphQL或结构化查询构建器,在边缘层就把用户输入转换成抽象语法树,再序列化为参数化语句。
总之,ORM产生SQL注入不是因为框架靠不住,而是原生接口被当作普通字符串函数使用。避开原生查询接口是一种简单粗暴的缓解手段,但真正可持续的方案是理解数据库协议边界、坚持参数化并约束动态标识符。这样即便不得不写原生SQL,也能把风险关进笼子里。