权限验证失效与SQL注入同时出现,说明应用在“身份校验”和“数据查询”两个关键环节都失去了约束。攻击者一旦绕过权限检查,后续拼接进SQL语句的恶意参数就能直接读取、篡改或删除数据库内容。要解决这个问题,不能只盯着SQL注入过滤,必须把“权限验证失败后为什么还能继续执行查询”这条链路一起堵死。
很多系统在登录后只做一次权限判断,比如用中间件检查用户角色,但后续业务代码里又直接拼接用户传入的id、type、order等参数执行SQL。一旦中间件逻辑存在漏洞,例如只校验了菜单权限没校验数据权限,或者校验失败后仅返回提示但未终止请求,SQL注入就有了可乘之机。更常见的情况是接口本身未纳入权限体系,例如某些内部接口、导出接口、报表接口,开发时为了方便直接暴露,攻击者扫描到后直接构造注入参数。
先修复权限验证失效的根因权限验证必须做到“失败即终止”。在控制器层或服务层入口,任何权限校验不通过的情况都应立即抛出异常并中断后续流程,不能使用if判断后仅return一个提示字符串。下面是一个容易出问题的写法:
if (!checkPermission(userId, "order:view")) {
return "无权限";
}
String sql = "select * from orders where id=" + request.getParameter("id");
这种写法中,如果checkPermission函数本身因为异常返回了true,或者调用方忽略了返回值,后续SQL依然会执行。正确做法是让权限校验函数在失败时直接抛出异常:
if (!checkPermission(userId, "order:view")) {
throw new PermissionDeniedException("无权访问订单数据");
}
同时,权限校验不能只依赖前端隐藏按钮或菜单。服务端必须对每个敏感接口独立校验,尤其是涉及数据查询、导出、删除、修改的操作。对于水平越权,还需要校验当前用户是否拥有该数据的所有权,例如订单详情接口中,除了校验“订单查看”权限,还要校验该订单是否属于当前用户或当前用户所在组织。
SQL注入的防护不能依赖单一过滤权限修复后,SQL注入本身仍需独立防护。最常见的错误是只对单引号、双引号做转义,或者使用简单的正则替换。攻击者可以通过编码绕过、宽字节注入、二次注入等方式突破这类过滤。例如对用户输入做addslashes或replace单引号,在GBK编码下仍可能被%df%27绕过。
参数化查询是防SQL注入最可靠的手段。所有拼接用户输入的SQL都应改为预编译语句。以Java JDBC为例:
String sql = "select * from orders where user_id = ? and order_id = ?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setInt(1, currentUserId);
ps.setString(2, request.getParameter("orderId"));
ResultSet rs = ps.executeQuery();
参数化查询将SQL结构与数据分离,数据库驱动会把参数作为字面值处理,而不是SQL代码的一部分。即使参数中包含恶意内容,也不会改变SQL语句的逻辑。对于MyBatis,应使用#{}而不是${}。${}会直接拼接字符串,等同于裸写SQL拼接,必须全面排查替换。
动态表名、列名、排序字段的处理参数化查询无法直接绑定表名、列名、排序方向等SQL标识符。很多列表接口会根据用户传入的sortField和sortOrder动态拼接order by子句,这也是SQL注入高发点。攻击者可能传入sortField为“id; drop table orders--”或“if(1=1,id,user_id)”。
处理这类场景必须使用白名单校验。后端定义允许排序的字段集合和排序方向,用户传入的值必须在白名单内才允许拼接:
private static final SetALLOWED_SORT_FIELDS = Set.of("id", "order_no", "create_time", "amount"); private static final Set ALLOWED_SORT_DIRECTIONS = Set.of("ASC", "DESC"); public String buildOrderBy(String sortField, String sortDirection) { if (sortField == null || !ALLOWED_SORT_FIELDS.contains(sortField)) { throw new IllegalArgumentException("非法的排序字段"); } if (sortDirection == null || !ALLOWED_SORT_DIRECTIONS.contains(sortDirection.toUpperCase())) { throw new IllegalArgumentException("非法的排序方向"); } return sortField + " " + sortDirection.toUpperCase(); }
对于动态表名,同样需要维护允许的表名集合,或者通过映射关系将用户传入的逻辑名称转换为实际表名,绝不可直接将用户输入拼接到SQL中。
ORM框架也不能完全依赖很多人认为使用了Hibernate、MyBatis-Plus等ORM框架就天然安全,实际上ORM只是减少了手写SQL,但原生SQL或动态查询接口仍可能引入注入。例如JPA的nativeQuery、MyBatis的${}、MyBatis-Plus的apply()方法,这些地方如果直接拼接用户输入,依然存在注入风险。
使用ORM时,应优先使用框架提供的条件构造器或命名参数。对于必须使用原生SQL的场景,同样要使用参数绑定。MyBatis-Plus中,last()方法用于拼接SQL片段,如果传入用户可控内容,必须进行严格白名单校验或直接禁止使用。
多层防御:输入验证与输出编码参数化查询解决了SQL注入的核心问题,但输入验证仍可作为第一道防线。对于数字型参数,应强制转换为整数或长整数;对于字符串型参数,应限制长度和字符集。例如订单号通常由数字和字母组成,可以用正则限制:
if (!orderNo.matches("^[A-Za-z0-9]{10,32}$")) {
throw new IllegalArgumentException("订单号格式非法");
}
输出编码主要防止存储型XSS,但在某些场景下,如果数据库中被写入了恶意脚本,查询后直接输出到页面也会造成二次危害。对输出到HTML、JavaScript、JSON的数据分别进行相应编码,可以降低注入成功后的影响范围。
权限验证与SQL注入的联动防护权限验证失效和SQL注入经常被组合利用。例如攻击者先通过越权访问一个本不该访问的接口,再在该接口中注入SQL获取更多数据。因此,防护策略需要将两者联动考虑:
第一,所有数据库操作都应经过统一的数据访问层。在数据访问层入口再次校验当前用户身份和资源归属,即使上层权限校验被绕过,数据访问层仍能拦截越权请求。例如在查询订单前,强制加上user_id条件,而不是仅依赖传入的订单id。
第二,记录并监控权限验证失败和SQL异常日志。权限验证失败可能意味着攻击者在探测接口,SQL异常则可能表明注入尝试。对短时间内大量权限失败或SQL语法错误的来源IP、用户账号进行告警和临时封禁,可以及时发现攻击行为。
第三,数据库账号遵循最小权限原则。应用连接数据库的账号不应具有DROP、CREATE、ALTER等DDL权限,只授予必要的SELECT、INSERT、UPDATE、DELETE权限。即使SQL注入成功,攻击者也无法直接删除表或修改表结构,降低了破坏程度。
代码审计与自动化检测权限验证和SQL注入的漏洞往往在代码层面隐藏较深,需要结合人工审计和自动化工具。审计重点包括:所有Controller和Service入口是否都有权限注解或显式校验;所有SQL语句是否使用了参数绑定;所有动态SQL标识符是否有白名单;所有原生SQL和框架动态拼接点是否可控。
自动化检测可以使用静态代码扫描工具,针对SQL拼接模式、危险函数调用、权限注解缺失等进行规则匹配。动态测试则可以使用SQLMap等工具对接口进行注入测试,但需注意测试环境与生产环境隔离。在CI/CD流程中集成安全扫描,可以在代码合并前发现大部分问题。
权限验证失效引发SQL注入的本质是纵深防御缺失。单点修复无法彻底解决问题,必须从权限校验的强制终止、参数化查询的全面覆盖、动态标识符的白名单控制、数据访问层的二次校验、数据库账号的最小权限以及持续的审计监控多个层面同时加固。任何一个环节的疏忽,都可能让攻击者找到新的突破口。
