Odata协议允许客户端通过URL参数灵活定义数据查询条件,这种灵活性如果被滥用,就会成为SQL注入的突破口。当服务端直接将Odata查询参数拼接进SQL语句执行时,攻击者可以构造包含恶意SQL片段的参数,实现非授权数据查询、篡改甚至删除操作。

Odata引发SQL注入的常见场景
最常见的风险场景是服务端未对Odata的$filter参数做校验,直接将其拼接进SQL的WHERE条件中。比如用户传入的$filter参数为id eq 1 or 1=1,如果服务端直接拼接,就会执行返回所有数据的SQL语句。
还有部分场景是Odata的$orderby、$select参数被恶意利用,攻击者通过构造特殊参数注入排序字段或查询字段,绕过权限校验获取敏感数据。
配置库层级查询解析器的设计思路
配置库层级的查询解析器核心是将查询规则的校验、解析逻辑从业务代码中抽离,放到统一的配置库管理,所有Odata请求先经过解析器处理,再生成安全的SQL语句。整体设计包含三个核心部分:
- 规则配置库:存储允许查询的实体、字段、操作符、函数白名单,以及不同用户的查询权限配置
- 解析引擎:负责解析Odata查询参数,校验语法合法性,匹配配置库规则
- SQL生成模块:基于校验通过的解析结果,生成参数化SQL语句,避免字符串拼接
核心实现步骤
1. 搭建规则配置库
首先需要设计配置表存储查询规则,核心表结构如下:
| 表名 | 字段 | 说明 |
|---|---|---|
| odata_entity_config | entity_name, allow_query_fields, allow_operators, allow_functions | 配置每个实体允许的查询字段、操作符、函数白名单 |
| odata_user_permission | user_role, entity_name, max_result_count, filter_restrict | 配置不同角色用户的查询权限限制 |
2. 实现Odata查询解析逻辑
解析器需要先解析Odata参数的语法树,再逐层校验是否符合配置库规则。以下是Java语言实现的解析核心逻辑示例:
import java.util.List;
import java.util.ArrayList;
// Odata查询节点定义
class OdataQueryNode {
String nodeType; // filter/orderby/select等
String content; // 节点内容
List<OdataQueryNode> children; // 子节点
public OdataQueryNode(String nodeType, String content) {
this.nodeType = nodeType;
this.content = content;
this.children = new ArrayList<>();
}
}
// 查询解析器核心类
public class OdataQueryParser {
// 解析Odata查询参数,返回语法树
public OdataQueryNode parse(String odataQuery) {
OdataQueryNode root = new OdataQueryNode("root", "");
// 拆分不同查询参数,比如$filter、$orderby、$select
String[] params = odataQuery.split("&");
for (String param : params) {
if (param.startsWith("$filter=")) {
String filterContent = param.substring(8);
OdataQueryNode filterNode = new OdataQueryNode("filter", filterContent);
// 递归解析filter内容的语法结构
parseFilterNode(filterNode, filterContent);
root.children.add(filterNode);
} else if (param.startsWith("$orderby=")) {
String orderbyContent = param.substring(9);
OdataQueryNode orderbyNode = new OdataQueryNode("orderby", orderbyContent);
root.children.add(orderbyNode);
} else if (param.startsWith("$select=")) {
String selectContent = param.substring(8);
OdataQueryNode selectNode = new OdataQueryNode("select", selectContent);
root.children.add(selectNode);
}
}
return root;
}
// 解析filter节点的语法
private void parseFilterNode(OdataQueryNode node, String content) {
// 处理逻辑操作符:eq/ne/gt/lt/ge/le/and/or/not
// 处理函数:contains/startswith/endswith等
// 此处省略具体语法解析实现,核心是拆分出字段、操作符、值三个部分
}
// 校验解析后的语法树是否符合配置库规则
public boolean validate(OdataQueryNode root, String entityName, String userRole) {
// 1. 查询配置库获取实体允许的规则
List<String> allowFields = getAllowFieldsFromConfig(entityName);
List<String> allowOperators = getAllowOperatorsFromConfig(entityName);
// 2. 遍历语法树校验每个节点
for (OdataQueryNode child : root.children) {
if ("filter".equals(child.nodeType)) {
if (!validateFilterNode(child, allowFields, allowOperators)) {
return false;
}
} else if ("orderby".equals(child.nodeType)) {
if (!validateOrderbyNode(child, allowFields)) {
return false;
}
} else if ("select".equals(child.nodeType)) {
if (!validateSelectNode(child, allowFields)) {
return false;
}
}
}
// 3. 校验用户权限限制
return validateUserPermission(entityName, userRole, root);
}
// 从配置库获取允许的查询字段
private List<String> getAllowFieldsFromConfig(String entityName) {
// 实际实现中查询odata_entity_config表
return new ArrayList<>();
}
// 校验filter节点
private boolean validateFilterNode(OdataQueryNode node, List<String> allowFields, List<String> allowOperators) {
// 校验字段是否在白名单、操作符是否在白名单、值是否合法(无SQL特殊字符)
return true;
}
// 校验orderby节点
private boolean validateOrderbyNode(OdataQueryNode node, List<String> allowFields) {
// 校验排序字段是否在白名单
return true;
}
// 校验select节点
private boolean validateSelectNode(OdataQueryNode node, List<String> allowFields) {
// 校验查询字段是否在白名单
return true;
}
// 校验用户权限
private boolean validateUserPermission(String entityName, String userRole, OdataQueryNode root) {
// 校验用户是否有查询该实体的权限、结果数量是否超过限制
return true;
}
}
3. 生成参数化SQL语句
校验通过的查询条件,需要生成参数化SQL,避免字符串拼接。以下是SQL生成的核心示例:
public class SafeSqlGenerator {
// 生成参数化SQL
public String generateSql(OdataQueryNode root, String tableName) {
StringBuilder sql = new StringBuilder("SELECT ");
StringBuilder where = new StringBuilder(" WHERE 1=1 ");
List<Object> params = new ArrayList<>();
// 处理select部分
for (OdataQueryNode child : root.children) {
if ("select".equals(child.nodeType)) {
sql.append(child.content.replace(" ", ""));
break;
}
}
if (sql.toString().endsWith("SELECT ")) {
sql.append("* ");
}
sql.append(" FROM ").append(tableName);
// 处理filter部分,生成参数化条件
for (OdataQueryNode child : root.children) {
if ("filter".equals(child.nodeType)) {
parseFilterToSql(child, where, params);
break;
}
}
sql.append(where);
// 实际执行时使用params列表做参数绑定,避免注入
return sql.toString();
}
// 将filter节点转换为SQL条件和参数
private void parseFilterToSql(OdataQueryNode node, StringBuilder where, List<Object> params) {
// 解析filter内容,将字段、操作符、值拆分,值用?占位,放入params列表
// 比如id eq 1 转换为 id = ?,params添加1
}
}
落地注意事项
首先配置库的规则需要支持动态更新,当新增实体或修改查询权限时,不需要重启服务即可生效。其次解析器需要覆盖Odata协议的所有常用查询参数,避免遗漏风险点。另外需要定期审计配置库的规则,及时移除不再使用的实体、字段的白名单配置,缩小攻击面。
最后建议在解析器外层增加请求频率限制,避免攻击者通过大量恶意请求探测系统漏洞,进一步提升整体安全性。