数据库注入攻击之所以到今天仍然高发,不是因为开发者不知道SQL注入这个概念,而是大量防护措施停留在“过滤几个关键字”或“拼接参数时加个引号”的层面。真正有效的专项防护,必须从代码层、数据库层、网络层和运维层同时收紧,并且默认所有外部输入都不可信。下面直接拆解每一层具体怎么做,以及哪些常见做法实际是无效的。

一、先认清注入发生的真实位置

很多人以为只要用户提交表单里的单引号被过滤掉就安全了,但注入点远不止表单。URL参数、Cookie、HTTP头(如User-Agent、Referer、X-Forwarded-For)、文件上传后的文件名、API接口的JSON字段、甚至日志系统拼接的内容,都可能成为注入入口。只要一段数据最终被拼进SQL语句,而这段数据来自客户端或任何非完全可信的边界,就必须按注入风险处理。防护的第一原则不是“找危险字符”,而是“不拼接”。

二、参数化查询是代码层的底线

参数化查询(Prepared Statement)是目前最可靠的防注入手段。它的核心逻辑是把SQL结构固定下来,用户输入只作为参数值传入,数据库不会把参数内容当作SQL语法解析。以Java的JDBC为例:

String sql = "SELECT id, username FROM users WHERE email = ? AND status = ?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, userEmail);
ps.setInt(2, 1);
ResultSet rs = ps.executeQuery();

这里即使userEmail中包含单引号、分号或“OR 1=1”,也只会被当作普通字符串匹配,不会改变SQL结构。PHP的PDO、Python的PyMySQL、.NET的SqlCommand都支持同样的机制。需要特别注意的是,参数化查询不能只用在“明显危险”的查询上,所有带外部输入的SQL都必须使用,包括UPDATE、DELETE、INSERT以及ORDER BY、LIMIT后面的动态值。ORDER BY和LIMIT在部分数据库驱动中不支持参数占位符,这时必须使用白名单校验列名和数值范围,绝不能直接拼接。

三、ORM不能替代参数化意识

很多团队认为用了Hibernate、MyBatis、Entity Framework这类ORM框架就自动防注入了,这是误解。ORM的实体查询确实会参数化,但原生SQL或动态查询接口依然存在风险。MyBatis中的${}会直接拼接字符串,而#{}才会预编译。Hibernate的HQL如果使用字符串拼接,同样可以被注入。因此使用ORM时,必须禁止在业务代码中出现字符串拼接SQL,并对所有原生SQL接口做集中审查。框架只能减少手写SQL的数量,不能替代开发者的安全意识。

四、存储过程不是万能盾牌

存储过程如果内部使用参数化调用,安全性等同于预编译语句。但如果存储过程内部使用EXEC或sp_executesql拼接字符串,风险依然存在。例如SQL Server中:

CREATE PROCEDURE GetUser
    @Email NVARCHAR(100)
AS
BEGIN
    DECLARE @sql NVARCHAR(MAX)
    SET @sql = 'SELECT * FROM users WHERE email = ''' + @Email + ''''
    EXEC(@sql)
END

这段存储过程就是典型的注入漏洞。正确做法是直接在存储过程内使用参数化查询,或者用sp_executesql带参数执行。所以存储过程本身不构成防护,关键还是看内部是否拼接。

五、输入验证与输出编码要分清边界

输入验证是防注入的辅助手段,但不能作为主要防线。对邮箱、手机号、数字ID等格式固定的字段,可以在进入业务逻辑前做严格白名单校验,格式不符直接拒绝。这能降低攻击面,但不能依赖黑名单过滤。常见的“过滤单引号、分号、注释符”做法很容易被编码绕过,比如攻击者使用十六进制编码、双重URL编码、Unicode同形字符或数据库特有的字符串连接函数。真正可靠的输入验证是“只允许预期格式通过”,而不是“把危险字符删掉”。

输出编码主要解决的是XSS问题,但和注入防护也有交叉。当数据库中的恶意内容被读出并再次拼入其他SQL或页面时,没有输出编码会形成二次注入。二次注入的典型场景是:用户第一次提交的数据被安全存储,但在后续查询中被直接拼进SQL语句。因此,所有从数据库读出的数据,如果还要进入新的SQL语句,同样必须参数化。

六、数据库账号权限最小化

即使代码层出现漏洞,数据库权限配置也能大幅降低损失。应用连接数据库的账号不应使用root、sa或sysadmin等高权限身份。按业务模块拆分数据库账号,例如前台查询账号只授予相关表的SELECT权限,后台管理账号才授予INSERT、UPDATE、DELETE权限,且禁止DDL权限。对于敏感表,可以进一步使用视图限制列级访问。这样即使注入成功,攻击者也无法直接执行DROP TABLE、xp_cmdshell或读取系统表。MySQL中可以通过以下语句创建最小权限账号:

CREATE USER 'app_read'@'%' IDENTIFIED BY 'StrongPassword';
GRANT SELECT ON shop.products TO 'app_read'@'%';

同时要定期审计账号权限,清理不再使用的账号,避免权限蔓延。

七、数据库自身的安全配置

数据库层面的配置加固经常被忽略。MySQL、SQL Server、Oracle等数据库默认安装后,部分功能会扩大攻击面。例如MySQL的LOAD_FILE、INTO OUTFILE函数允许读写服务器文件,SQL Server的xp_cmdshell可以执行系统命令。如果业务不需要这些功能,应显式禁用或限制。MySQL可以通过secure_file_priv参数限制文件读写目录,SQL Server可以通过sp_configure关闭xp_cmdshell。此外,数据库错误信息不应直接返回给前端,详细的报错信息会暴露表名、列名和数据库版本,极大降低攻击难度。应用层要统一捕获异常,对外只返回通用错误提示,详细错误只记录到服务端日志。

八、Web应用防火墙的规则要针对SQL语义

WAF可以作为网络层的补充防线,但不能只靠通用规则。有效的WAF规则应针对SQL语句的语义特征,而不是简单匹配单引号或SELECT关键字。例如检测“UNION SELECT”、“AND 1=1”、“WAITFOR DELAY”、“SLEEP(”等组合模式,同时结合请求上下文判断。对于JSON、XML格式的API请求,WAF需要支持解析结构化数据后再检测,否则攻击者把payload放在多层嵌套字段中就能绕过。WAF还应开启频率限制和IP信誉库,对短时间内大量异常请求的源IP自动拦截。但WAF不能替代代码修复,它只是争取响应时间的缓冲层。

九、日志监控要能发现注入尝试

很多注入攻击在成功前会有大量探测行为,比如请求参数中出现单引号、分号、注释符、UNION关键字等。如果日志系统只记录访问URL而不记录完整参数,这些探测很难被发现。建议在应用日志中记录完整的请求参数、数据库执行耗时和返回行数,并对包含SQL关键字的请求单独标记。数据库端可以开启慢查询日志和审计日志,重点监控执行时间异常、返回行数异常、包含系统表访问的SQL。日志集中分析平台可以设置规则,当同一IP在短时间内触发多条SQL注入特征请求时自动告警。日志的价值不仅在于事后溯源,更在于发现正在进行的攻击。

十、专项测试要覆盖绕过手法

常规的功能测试很难发现注入漏洞,必须进行专项安全测试。测试用例应覆盖常见绕过手法:大小写混写(SeLeCt)、内联注释(//)、双重编码(%2527)、宽字节注入、二次编码、HTTP参数污染、分块传输编码等。测试时不仅要测GET和POST参数,还要测Cookie、HTTP头、文件上传文件名、API路径参数。自动化扫描工具如SQLMap可以快速发现基础漏洞,但复杂业务逻辑中的二次注入和盲注需要人工分析。建议在每次上线前执行一轮注入专项测试,并在CI/CD流程中加入静态代码扫描,重点检查字符串拼接SQL的模式。

十一、老旧系统无法全面改造时的过渡方案

现实中存在大量遗留系统,代码量巨大,短期内无法全部改为参数化查询。对于这类系统,可以优先在高风险入口部署数据库代理层的过滤机制。例如在应用与数据库之间增加一层代理,对SQL语句做语法解析,识别并阻断包含明显注入特征的语句。同时,对数据库账号做紧急权限收缩,禁用危险函数,并加强WAF规则。过渡方案不能作为长期依赖,但可以在系统重构前降低被攻破的概率。更务实的做法是优先修复登录、搜索、订单查询等核心入口,因为这些位置一旦被注入,影响范围最大。

十二、开发规范要明确禁止事项

防注入不能只靠个人自觉,必须在开发规范中明确写出禁止和必须执行的事项。禁止事项包括:禁止使用字符串拼接构造SQL、禁止在MyBatis中使用${}、禁止在存储过程中使用EXEC拼接、禁止将数据库错误信息返回前端。必须执行的事项包括:所有外部输入必须参数化、动态列名和排序字段必须走白名单、数据库账号必须最小权限、所有SQL变更必须经过安全评审。规范要落实到代码评审清单中,每次提交代码时逐项检查。对于新项目,可以在框架层封装统一的数据库访问接口,从技术上限制开发者只能使用参数化方法。

数据库防注入攻击的专项防护,本质上是一个纵深防御体系。参数化查询解决根本问题,输入验证和WAF降低攻击面,数据库权限和配置限制损失范围,日志监控提供发现能力,专项测试验证防护效果。任何单点措施都可能被绕过,但多层叠加后,攻击者的成本和难度会成倍增加。真正有效的防护不是某一种技术,而是让“不信任任何输入”成为开发、运维、安全团队共同遵守的默认规则。