前后端分离架构下,SQL注入的防护重心从传统的“输出转义”全面转向了API层的参数校验与持久层参数化查询。很多人以为用了Vue或React前端、后端只返回JSON就不会有注入问题,实际上攻击面只是转移了,并没有消失。接口一旦暴露,攻击者完全可以绕过前端直接向后端API发送构造好的恶意参数,如果后端只做了简单的字符串拼接SQL,注入风险甚至比传统服务端渲染更高,因为接口数量多、参数结构复杂、权限边界模糊,漏掉一个入口就是灾难。

先定位前后端分离场景下SQL注入的真正入口

在前后端分离架构中,前端不再直接拼接SQL,这是事实。但后端接收的每一个HTTP请求参数、路径变量、请求体JSON字段、甚至Header中的某些业务字段,都可能成为注入载体。常见的漏洞点集中在:列表查询接口中的排序字段、分页参数、模糊搜索关键字、批量删除的ID数组、报表导出的过滤条件、以及一些“万能查询”接口。这些接口为了灵活,经常允许前端传入字段名、排序方向、操作符等内容,后端如果把这些内容直接拼进SQL语句,就形成了典型的注入通道。

例如一个订单列表接口,前端可能传递如下参数:

{
  "page": 1,
  "size": 20,
  "sortField": "create_time",
  "sortOrder": "desc",
  "keyword": "手机"
}

如果后端在处理排序时写成这样:

String sql = "SELECT * FROM orders WHERE product_name LIKE '%" + keyword + "%' ORDER BY " + sortField + " " + sortOrder + " LIMIT " + (page-1)*size + "," + size;

那么攻击者只需要将sortField的值改为“create_time; DROP TABLE orders;--”或者使用报错注入、时间盲注等手段,就能直接控制数据库。这种漏洞在代码审计中极其常见,原因就是开发者默认前端只会传合法值,忽略了API可以直接被脚本调用。

参数化查询是底线,但不是全部

所有涉及数据库操作的SQL语句,必须使用预编译参数化查询。在Java中对应PreparedStatement,在Go中对应database/sql的占位符,在Python中对应cursor.execute的参数绑定。参数化查询能从根本上杜绝将用户输入作为SQL代码执行的可能。但这里有一个关键细节:参数化查询只能用于值的位置,不能用于表名、字段名、排序方向、LIMIT等结构位置。也就是说,上面例子中的keyword可以用参数化绑定,但sortField和sortOrder必须通过白名单校验后才能拼接。

正确的做法是,后端维护一个允许排序的字段映射表:

Map<String, String> allowedSortFields = Map.of(
    "createTime", "create_time",
    "orderNo", "order_no",
    "amount", "amount"
);
String sortField = allowedSortFields.getOrDefault(request.getSortField(), "create_time");
String sortOrder = "desc".equalsIgnoreCase(request.getSortOrder()) ? "DESC" : "ASC";

这样无论攻击者传入什么内容,最终参与SQL拼接的只能是白名单内的固定值。排序方向同理,只允许ASC和DESC两种。LIMIT和OFFSET必须强制转换为整数,并且设置最大值限制,防止攻击者通过超大分页拖垮数据库。

ORM框架不是银弹,动态查询要格外小心

很多团队在前后端分离项目中使用MyBatis、JPA、Hibernate、GORM等ORM框架,认为框架会自动防注入。实际上,ORM只能在你正确使用其参数绑定特性时提供保护。MyBatis中的${}和#{}的区别就是经典分界线:#{}会走预编译参数绑定,而${}是直接字符串替换,等同于拼接SQL。在动态SQL标签中,如果使用了${}来拼接字段名或排序内容,注入风险依旧存在。

更隐蔽的是JPA的Criteria API或QueryDSL,虽然它们本身是类型安全的,但很多开发者为了省事,会直接使用原生SQL或JPQL拼接。例如:

String jpql = "SELECT o FROM Order o WHERE o.productName LIKE '%" + keyword + "%'";

这种写法在代码中大量存在,尤其是老项目迁移到前后端分离架构时,后端代码往往原封不动地保留了旧有的拼接习惯。所以,无论使用什么框架,最终都要检查实际生成的SQL是否包含未经处理的用户输入。可以通过日志开启SQL打印,观察参数是否以绑定形式出现。

API层的输入校验要前置且严格

前后端分离后,后端的每一个接口都必须独立完成输入校验,不能依赖前端。前端校验只是用户体验层面的第一道过滤,真正的安全边界在后端。校验内容包括:参数类型、长度、格式、取值范围、是否允许为空、是否包含特殊字符等。对于搜索关键字这类自由文本,除了长度限制外,还应该根据业务需求过滤或转义SQL通配符,例如百分号、下划线、反斜杠等,防止攻击者利用LIKE查询进行资源消耗型攻击。

对于JSON请求体,推荐使用DTO对象接收,并在字段上添加校验注解。以Spring Boot为例:

public class OrderQueryRequest {
    @Min(1)
    private Integer page = 1;
    
    @Max(100)
    private Integer size = 20;
    
    @Pattern(regexp = "^(createTime|orderNo|amount)$", message = "非法的排序字段")
    private String sortField;
    
    @Pattern(regexp = "^(asc|desc)$", message = "非法的排序方向")
    private String sortOrder;
    
    @Size(max = 50, message = "关键字长度不能超过50")
    private String keyword;
}

这种校验在进入Service层之前就拦截了大部分异常输入。但要注意,正则校验只能作为第一层过滤,不能替代白名单映射。因为正则可能因为书写不严谨被绕过,而白名单是精确匹配,安全性更高。

数据库权限最小化与纵深防御

即使应用层做好了参数化和白名单,也应该在数据库层面实施纵深防御。前后端分离架构中,后端服务通常使用一个统一的数据库账号连接数据库,这个账号往往拥有过高权限。一旦某个接口被注入成功,攻击者就能利用这个高权限账号执行危险操作。正确的做法是:为不同的业务模块创建不同的数据库账号,只授予必要的SELECT、INSERT、UPDATE、DELETE权限,禁止DDL权限,禁止访问系统表。对于敏感数据,还可以使用数据库视图或字段级加密进一步限制暴露面。

此外,数据库连接池的账号密码应该通过配置中心或环境变量注入,不能硬编码在代码或配置文件中。定期审计数据库账号权限,回收不再使用的账号,也是降低注入危害的有效措施。

WAF与日志监控在API场景下的特殊配置

传统WAF规则大多针对表单提交和URL参数,对JSON请求体的解析能力有限。前后端分离后,大量参数放在JSON body中,攻击者也会利用这一点绕过只检查URL和表单的WAF。因此,在部署WAF时,必须开启JSON解析和深度检测功能,针对API路径设置专门的规则。同时,WAF不能作为唯一的防线,它只能拦截已知的攻击模式,对于绕过WAF的高级注入手法无能为力。

更有效的做法是在应用层增加SQL注入检测日志。每当检测到请求参数中包含SQL关键字或异常字符时,记录完整的请求上下文,包括IP、用户ID、请求路径、完整参数、时间戳等。这些日志不仅可以用于事后溯源,还能通过实时分析发现正在进行的攻击行为。例如,可以监控单IP在短时间内请求大量包含“union select”、“sleep(”、“benchmark(”等特征的接口,触发告警并自动封禁。

针对前后端分离架构的代码审计要点

在安全审计时,应该重点关注以下几类代码模式:第一,所有使用字符串拼接构建SQL的地方,包括MyBatis XML中的${}、Java中的StringBuilder拼接、以及任何直接执行原生SQL的代码。第二,所有接收前端参数后直接用于排序、分组、表名、字段名的场景。第三,所有批量操作接口,例如批量删除、批量更新,这些接口经常使用IN子句,如果拼接不当,注入风险极高。第四,所有导出接口,因为导出功能通常需要拼接复杂的过滤条件,而且容易忽略参数校验。

对于IN子句的参数化,不同语言和框架有不同处理方式。在MyBatis中可以使用foreach标签配合#{}实现参数绑定:

SELECT * FROM orders WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
    #{id}
</foreach>

这种写法是安全的,因为每个id都作为参数绑定,而不是字符串拼接。但要注意,ids集合本身不能来自前端传入的字符串直接split后使用,必须经过类型转换和数量限制。

前端安全措施在整体防护中的辅助作用

虽然前端不能作为安全边界,但在前后端分离架构中,前端仍然可以承担一部分辅助防护工作。例如,在前端请求拦截器中统一过滤明显的SQL关键字和特殊字符,可以减少大量自动化扫描器的无效请求。同时,前端可以对用户输入进行长度限制和格式校验,降低后端处理异常数据的压力。但这些措施必须明确其定位:只是减少噪音,不能替代后端防护。攻击者可以轻松修改前端代码或直接使用curl、Postman等工具绕过前端限制。

另一个容易被忽视的点是,前后端分离项目中,前端代码通常部署在独立的静态资源服务器或CDN上,如果前端代码中泄露了API接口的详细参数结构、字段名、甚至调试信息,会大大降低攻击者的探测成本。因此,前端构建时应移除所有调试日志、注释中的敏感信息,并对API接口路径进行合理混淆或网关层访问控制。

建立持续防护机制而非一次性修复

SQL注入防护不是一次代码修复就能一劳永逸的事情。前后端分离架构迭代速度快,接口频繁变更,新的注入点会不断出现。因此,必须将SQL注入检测纳入CI/CD流水线。在代码提交阶段,使用静态代码扫描工具自动识别SQL拼接、${}使用、危险函数调用等问题。在测试阶段,集成自动化安全测试,对API接口进行模糊测试,模拟注入攻击。在发布后,持续监控数据库慢查询日志和错误日志,异常的SQL执行模式往往是注入攻击的痕迹。

对于已经发现并修复的注入漏洞,要建立漏洞案例库,将漏洞成因、修复方案、影响范围记录在案,并在团队内进行分享。很多注入漏洞的根源是开发者对参数化查询的理解不够深入,或者对框架特性不熟悉。通过持续的培训和技术分享,可以从根本上减少同类问题的复发。

前后端分离架构下的SQL注入防护,核心在于将安全边界从传统的页面层后移到API层,以参数化查询为基础,以白名单校验为关键,以输入校验、权限最小化、日志监控、代码审计为纵深防御手段。只有每一层都落实到位,才能在快速迭代的接口开发中守住数据库的安全底线。