SQL注入漏洞之所以常年位居Web安全风险榜首,根本原因在于它并非源于某种高深莫测的攻击技术,而是源于开发过程中对数据流控制的疏忽。代码审计的核心价值,就是在攻击者发现漏洞之前,用攻击者的思维逐行审视代码,把“用户输入”当作不可信数据源,追踪它如何进入SQL语句、是否经过有效过滤、最终是否构成可执行恶意逻辑的路径。提前介入审计,比事后修复的成本低一个数量级,也能从根本上降低业务上线后的数据泄露风险。

很多人误以为使用参数化查询或ORM框架就能完全杜绝SQL注入,实际上并非如此。参数化查询确实能解决大部分因字符串拼接导致的注入问题,但无法覆盖动态表名、动态列名、排序字段等场景。ORM框架在复杂查询或原生SQL片段中依然可能暴露风险。代码审计要做的是识别这些“看似安全”的盲区,而不是简单检查是否使用了预编译语句。

从数据流视角定位注入点

审计SQL注入的第一步,是梳理数据流。任何一个外部输入,无论是HTTP参数、请求头、Cookie、文件上传内容,还是从其他接口获取的数据,只要最终拼接到SQL语句中,就构成潜在注入点。审计时不能只看Controller层是否做了过滤,而要追踪到DAO层、Service层,确认数据在每一层是否被二次加工、是否被绕过过滤函数。

常见的危险模式包括:直接使用字符串拼接构造SQL语句;使用MyBatis时在XML映射文件中写${}而不是#{};在存储过程中使用动态SQL拼接;在Hibernate中使用HQL或原生SQL拼接参数。这些模式即使外层有过滤,也可能因为编码不一致、过滤不完整而被绕过。

识别过滤函数的真实有效性

代码审计中经常看到开发者使用addslashes()、mysql_real_escape_string()或自定义正则来过滤输入,但这些方法都存在局限性。例如addslashes()只处理单引号、双引号、反斜杠和NULL字符,在GBK编码环境下可能被宽字节注入绕过。更关键的是,如果SQL语句中参数没有用引号包裹,转义函数完全无效。审计时要验证过滤函数是否与数据库字符集匹配、是否覆盖所有特殊字符、是否在正确的上下文位置调用。

另一个容易被忽略的问题是二次注入。用户输入在第一次入库时被转义存储,之后从数据库读取出来再次拼接到SQL语句时,如果没有重新过滤,就会形成注入。审计时要特别关注“从数据库取数据再用于查询”的逻辑,这类漏洞往往隐藏得很深。

动态SQL拼接的审计要点

动态SQL是SQL注入的高发区。在MyBatis中,${}会直接将参数值替换到SQL语句中,不进行预编译处理。审计时如果发现${}用于拼接表名、字段名、排序方式等无法参数化的场景,需要确认这些值是否来自白名单校验。例如排序字段通常应该限制在固定集合内,而不是直接接受前端传入的任意字符串。

下面这段代码就是典型的危险写法:

// 危险:直接拼接排序字段
String orderBy = request.getParameter("orderBy");
String sql = "SELECT * FROM users ORDER BY " + orderBy;

正确的做法是使用白名单映射:

// 安全:白名单校验排序字段
Map<String, String> allowedOrderBy = new HashMap<>();
allowedOrderBy.put("id", "id");
allowedOrderBy.put("username", "username");
allowedOrderBy.put("create_time", "create_time");
String orderBy = request.getParameter("orderBy");
if (!allowedOrderBy.containsKey(orderBy)) {
    orderBy = "id";
}
String sql = "SELECT * FROM users ORDER BY " + allowedOrderBy.get(orderBy);

审计时看到任何动态拼接的SQL片段,都要追问:这个值从哪里来?是否经过白名单校验?如果绕过校验会发生什么?

存储过程中的注入风险

存储过程并不天然安全。如果存储过程内部使用EXEC、sp_executesql或动态拼接SQL语句,同样存在注入风险。审计存储过程时,要检查传入参数是否被直接用于构造SQL字符串。例如SQL Server中以下写法非常危险:

CREATE PROCEDURE GetUser
    @username NVARCHAR(50)
AS
BEGIN
    DECLARE @sql NVARCHAR(4000)
    SET @sql = 'SELECT * FROM users WHERE username = ''' + @username + ''''
    EXEC(@sql)
END

即使调用方使用了参数化查询,存储过程内部的动态拼接依然会触发注入。审计时需要打开存储过程源码,逐行检查动态SQL的构造逻辑。

ORM框架中的原生SQL与HQL注入

Hibernate、JPA等ORM框架虽然提供了安全的查询方式,但开发者经常为了性能或便利使用原生SQL。例如entityManager.createNativeQuery()或session.createSQLQuery(),如果直接拼接参数,同样会注入。HQL中如果使用字符串拼接而不是命名参数或位置参数,也会存在风险。

审计ORM相关代码时,要区分“安全API”和“危险API”。例如JPA的createQuery配合setParameter是安全的,但createNativeQuery配合字符串拼接就是危险的。还要注意JPQL中的LIKE查询,如果参数中包含了%或_通配符,虽然不构成注入,但可能导致查询逻辑异常,也需要在审计中标注。

宽字节注入与编码问题

宽字节注入主要出现在GBK等宽字节编码环境下。当数据库使用GBK编码,而PHP等语言使用addslashes()进行转义时,攻击者可以通过构造特殊字节序列,使转义符\与后续字节组合成一个合法的宽字节字符,从而“吃掉”转义符,使单引号逃逸出来。审计时需要确认数据库连接编码、页面编码、过滤函数是否统一使用UTF-8,避免因编码不一致导致过滤失效。

更隐蔽的情况是二次编码。如果应用对输入进行了URL解码或HTML实体解码后再拼接到SQL语句中,而过滤发生在解码之前,攻击者可以通过双重编码绕过过滤。审计时要关注数据在进入SQL语句前经历了哪些编码转换,过滤位置是否在最终解码之后。

自动化审计工具与人工分析结合

代码审计工具如Fortify、Checkmarx、SonarQube等能够快速扫描出明显的SQL注入模式,但误报率和漏报率都不低。工具的优势在于覆盖全面,能发现人工容易遗漏的路径;劣势在于无法理解业务上下文,难以判断某个动态拼接是否真的可控。最佳实践是先用工具扫描,再由安全人员对扫描结果进行人工确认,同时结合数据流分析手动追踪工具未覆盖的路径。

人工审计时,建议从入口点反向追踪。找到所有接收外部输入的地方,如request.getParameter()、@RequestParam、@PathVariable、$_GET、$_POST等,然后沿着数据流向下追踪,直到确认它是否进入SQL语句。对于大型项目,可以借助IDE的“查找引用”功能快速定位数据流向。

修复策略与防御纵深

审计发现SQL注入后,修复方案需要根据具体场景选择。对于值类型的参数,优先使用参数化查询或预编译语句。对于无法参数化的标识符,如表名、字段名,必须使用白名单校验。对于排序、分页等场景,同样需要白名单或类型强制转换。对于存储过程,应避免内部动态SQL,改为参数化调用。

除了修复单个漏洞,还应在架构层面建立防御纵深。例如在Web应用防火墙中配置SQL注入检测规则,在数据库账号权限上遵循最小权限原则,禁止应用账号拥有xp_cmdshell、LOAD_FILE等高危权限。同时,建立代码提交前的自动化安全扫描流程,将SQL注入检测集成到CI/CD流水线中,从源头减少漏洞进入生产环境的概率。

代码审计的价值不仅在于发现漏洞,更在于推动开发团队形成安全编码习惯。每一次审计报告中指出的SQL注入问题,都应该转化为团队内部的安全编码规范,例如“禁止使用字符串拼接构造SQL”、“动态标识符必须白名单校验”、“所有外部输入必须参数化处理”。当这些规则成为团队的共识,SQL注入风险才能真正被系统性降低。