API接口的SQL注入风险比传统网页表单更隐蔽,因为攻击者不再需要面对浏览器界面,而是直接构造恶意参数发送到服务器。很多开发者以为接口只给自家前端调用就安全了,实际上抓包工具能轻易截获请求并篡改参数,注入测试往往在几分钟内就能完成。真正要防住SQL注入,必须从接口设计、参数校验、SQL执行方式、数据库权限四个层面同时下手,而不是只靠某个单一过滤函数。
最核心的一条原则是:永远不要把用户输入直接拼接到SQL语句里。无论参数来自GET的查询字符串、POST的请求体、HTTP头还是Cookie,只要最终进入了SQL语句,就必须使用参数化查询。以Java的JDBC PreparedStatement为例,正确写法是使用问号占位符,再把用户输入作为参数绑定进去。数据库驱动会自动对参数值进行转义处理,从协议层面杜绝了SQL关键字被解析执行的可能。下面这段代码展示了安全的参数化查询写法:
String customerName = request.getParameter("customerName");
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, customerName);
ResultSet results = pstmt.executeQuery();
如果使用ORM框架,也不能掉以轻心。Hibernate的HQL、MyBatis的XML映射同样存在注入风险,尤其是在使用字符串拼接HQL或者MyBatis的${}符号时。MyBatis中应当强制使用#{}来引用参数,#{}会走PreparedStatement的参数绑定流程,而${}是直接替换字符串,等同于拼接SQL。代码审查时只要看到${}出现在SQL映射文件里,就应当视为高危问题并要求整改。
参数化查询解决了大部分注入问题,但有些场景无法使用参数绑定,比如动态表名、动态排序字段、动态分组条件。这类需求在API接口中很常见,例如前端传一个sortField参数让后端按不同字段排序。此时不能直接把sortField拼进ORDER BY子句,正确做法是建立白名单映射,只允许传入的值对应到预设的字段名。比如前端传“create_time_desc”,后端映射为“ORDER BY create_time DESC”,前端传“price_asc”,后端映射为“ORDER BY price ASC”。任何不在白名单中的值一律返回参数错误,这样即使攻击者传入“;DROP TABLE users--”也不会产生任何SQL执行效果。
输入校验是第二道防线。虽然参数化查询已经能阻止注入,但严格的输入校验可以提前拦截大量恶意请求,减少数据库压力,也能防止其他类型的攻击如XSS或命令注入。对于API接口,应当为每个参数定义明确的数据类型、长度范围、格式规则。比如用户ID必须是正整数,手机号必须匹配正则表达式,排序字段必须来自枚举集合。校验逻辑放在Controller层入口处,使用统一的校验框架如Spring Validation或自定义注解,校验失败直接返回400状态码和明确的错误信息,不进入后续业务逻辑。
数据库权限最小化是常被忽视的一环。很多API服务连接数据库时使用root账号或拥有全部权限的高权限账号,这等于给攻击者留了一扇后门。即使前面的防御全部失效,攻击者通过注入拿到了执行任意SQL的能力,如果数据库账号只有SELECT权限,他最多只能拖取数据,无法执行UPDATE、DELETE、DROP等破坏性操作。正确的做法是为每个API服务创建独立的数据库账号,只授予业务所需的最小权限。例如一个只读接口的账号只授予相关表的SELECT权限,一个写入接口的账号只授予INSERT和UPDATE权限,绝不授予DDL权限。同时禁用多语句执行,很多数据库驱动默认允许一次执行多条SQL语句,攻击者利用分号拼接就能执行额外语句,应当在连接字符串中关闭该选项。
存储过程并不能天然防注入。有些团队认为使用存储过程就安全了,这是一个危险的误解。如果存储过程内部仍然使用字符串拼接动态SQL,注入风险照样存在。以SQL Server为例,存储过程里使用sp_executesql执行拼接字符串时,如果没有正确参数化,同样会被注入。存储过程只有在内部也使用参数化查询时才安全。此外,存储过程往往拥有更高的数据库权限,一旦被注入,破坏范围反而更大。
对于API接口,还应当关注二阶SQL注入。这种攻击方式不是直接在请求参数中注入恶意代码,而是先把恶意数据存入数据库,之后某个业务逻辑读取这些数据并拼接到新的SQL语句中执行。例如用户在注册时提交的用户名包含注入代码,注册接口做了参数化处理所以安全入库,但后续一个生成报表的功能读取用户名并直接拼接SQL,就触发了注入。防御二阶注入的关键在于所有从数据库读取的数据在再次进入SQL语句时,同样要走参数化查询,不能因为数据“来自自己数据库”就放松警惕。
WAF可以作为一个辅助层,但不能作为主要防御手段。Web应用防火墙通过规则匹配识别常见的注入特征,比如单引号、UNION SELECT、sleep函数等。攻击者可以通过编码绕过、注释混淆、大小写变换等方式绕过WAF规则,而且API接口的JSON格式参数和传统表单参数不同,部分WAF对JSON体内的注入检测能力较弱。WAF适合用来拦截批量扫描和自动化攻击工具,降低噪音,但真正的安全必须依靠代码层面的参数化查询和严格校验。
日志监控与告警是最后一道防线。即使防御体系再完善,也不能保证百分之百不被突破。应当在API网关或应用层记录所有请求的关键信息,包括请求路径、参数、来源IP、响应状态码、执行耗时。对于数据库层面,开启慢查询日志和审计日志,记录所有执行过的SQL语句。当检测到大量包含注入特征的请求、异常的SQL错误信息、短时间内频繁的数据库报错时,触发告警通知安全团队。攻击者往往先进行探测,再实施注入,早期发现探测行为可以及时封禁来源IP并修补漏洞。
还有一个容易被忽略的细节:API接口的错误信息不能直接返回给客户端。很多框架在发生SQL异常时会把完整的SQL语句、表结构、数据库类型等信息返回到响应体中,这等于给攻击者提供了详细的侦察情报。应当统一异常处理,对外只返回模糊的错误提示如“请求参数不合法”或“服务内部错误”,详细的异常堆栈和SQL信息只记录在服务端日志中,供开发人员排查使用。
对于使用微服务架构的团队,API网关层可以统一做参数格式校验和恶意特征过滤,但网关校验不能替代各服务的独立防御。因为攻击者可能绕过网关直接访问内部服务,或者通过服务间调用传递恶意参数。每个服务都应当假设上游传来的数据是不可信的,独立完成参数校验和参数化查询。纵深防御的原则在这里同样适用:网关过滤一层,服务校验一层,数据访问层参数化一层,数据库权限限制一层,日志监控兜底一层。
实际落地时,建议将安全规范固化到开发流程中。代码评审时重点检查SQL拼接、MyBatis的${}使用、动态SQL拼接、数据库账号权限配置。CI/CD流水线中集成静态代码扫描工具,自动检测SQL注入风险模式。定期进行渗透测试,模拟攻击者对API接口进行注入测试,验证防御措施的有效性。安全是一个持续的过程,不是上线前做一次检查就一劳永逸。
总结来说,API接口防SQL注入的核心是参数化查询,辅以白名单校验、输入格式校验、数据库权限最小化、错误信息脱敏、日志监控告警。任何单一措施都不足以完全杜绝注入,只有多层防御叠加,才能把风险降到可接受的范围。开发者需要从写第一行接口代码开始就建立安全意识,而不是等被攻击后再修补。
