SQL注入至今仍是数据库面临的最致命威胁之一,攻击者通过构造畸形的SQL语句,绕过应用层逻辑直接操纵数据库,轻则数据泄露,重则整库被拖走甚至服务器沦陷。很多人以为使用参数化查询就万事大吉,但在真实生产环境中,MySQL的防护必须从连接层、SQL模式、权限模型、日志审计到部署架构形成纵深配置,才能把风险压到最低。下面直接讲具体怎么配、为什么这么配、以及哪些坑必须避开。
一、先收紧MySQL自身的SQL解析行为
MySQL有一个极其重要但经常被忽略的配置项:sql_mode。默认情况下,很多发行版并不会开启最严格的模式,这会导致一些危险的数据写入行为被静默容忍。比如在非严格模式下,向一个INT字段插入字符串'abc',MySQL只会抛出一个警告,然后写入0,而不是直接报错。这种宽容看似方便,实际上给注入攻击留下了操作空间,攻击者可以通过构造特殊值探测表结构,甚至绕过某些基于报错和类型判断的防护逻辑。
建议在my.cnf中明确设置:
[mysqld] sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION,ONLY_FULL_GROUP_BY'
其中STRICT_TRANS_TABLES是最核心的一项,它强制MySQL对事务型存储引擎执行严格的数据类型校验,非法值直接报错而不是截断或转换。ONLY_FULL_GROUP_BY则可以防止利用GROUP BY进行隐式数据推断。很多注入工具依赖MySQL的宽松行为来猜解列数和数据类型,开启严格模式后,大量自动化注入尝试会因为类型错误而中断,这本身就是一道有效的干扰层。
二、连接账户必须做权限原子化拆分
生产环境中最常见的错误是应用直接使用root或者一个拥有ALL PRIVILEGES的账户连接数据库。一旦注入发生,攻击者可以直接执行LOAD_FILE()读取系统文件、使用INTO OUTFILE写入Webshell、甚至通过UDF提权。权限控制不是可有可无的附加项,而是防注入的最后一道物理隔离。
正确的做法是为每个应用建立独立的最小权限账户,并且严格限定其可访问的库、表、字段和操作类型。例如一个只需要读取用户信息的服务,只授予SELECT权限,并且只针对特定表:
CREATE USER 'app_readonly'@'10.0.0.10' IDENTIFIED BY '强密码'; GRANT SELECT ON app_db.users TO 'app_readonly'@'10.0.0.10'; GRANT SELECT ON app_db.user_profile TO 'app_readonly'@'10.0.0.10'; FLUSH PRIVILEGES;
同时必须禁用FILE权限。FILE权限允许使用LOAD_FILE()和INTO OUTFILE,这是注入攻击中最常利用的提权路径之一。默认情况下MySQL不会授予FILE权限,但很多运维为了方便导入导出会手动加上,这是非常危险的习惯。如果确实需要文件导入,应该使用专门的管理账户在本地操作,而不是应用连接账户。
另外,限制账户的连接来源IP。不要使用'user'@'%'这种通配形式,而是精确到应用服务器所在的网段或具体IP。即使密码泄露,攻击者也无法从外部主机直接连接数据库。
三、预处理语句是底线,但需要配合输入边界控制
参数化查询(Prepared Statement)确实能从根本上消除绝大多数SQL注入,因为它将SQL逻辑与数据分离,数据部分永远不会被当作SQL代码解析。但现实中有几个问题需要注意。第一,不是所有查询都能参数化,比如动态排序字段ORDER BY、动态表名、动态列名,这些场景下参数化查询无法直接使用,必须通过白名单映射来处理。第二,存储过程中如果使用了动态SQL拼接,同样存在注入风险,不能因为用了存储过程就放松警惕。
对于无法参数化的标识符,必须建立严格的白名单:
// 伪代码示例:排序字段白名单
$allowedColumns = ['id', 'username', 'create_time', 'last_login'];
$orderBy = in_array($_GET['order'], $allowedColumns, true) ? $_GET['order'] : 'id';
$sql = "SELECT * FROM users ORDER BY {$orderBy} LIMIT ?";
同时,在应用层对输入数据做类型和长度校验,比如ID必须是正整数、用户名长度不超过32字节、邮箱必须匹配正则。这不是为了替代参数化查询,而是为了减少异常输入到达数据库层的概率,同时也能防御一些基于超长字符串的拒绝服务攻击。
四、部署数据库防火墙或SQL审计插件
MySQL官方提供了企业版的Firewall插件,但社区版用户可以使用第三方方案,比如Percona的审计插件或者开源的MySQL Proxy层过滤。数据库防火墙的核心价值在于它工作在SQL解析之后、执行之前,能够基于规则识别和阻断可疑的SQL模式,即使应用层存在漏洞,也能在数据库入口处拦截。
以MySQL Enterprise Firewall为例,它可以学习应用的正常SQL访问模式,建立白名单基线,之后任何偏离基线的SQL语句都会被拒绝。对于社区版,可以考虑使用ProxySQL作为中间层,通过mysql_query_rules实现SQL级过滤:
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (100, 1, 'UNION SELECT', 'Blocked potential SQL injection', 1); INSERT INTO mysql_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (101, 1, 'INFORMATION_SCHEMA', 'Blocked schema probing', 1);
这种方式虽然不能完全替代代码修复,但在应急场景下可以快速止血,尤其是当线上系统无法立即发布补丁时,通过ProxySQL规则可以直接阻断正在发生的注入攻击。
五、开启通用查询日志和慢查询日志的定向审计
很多人认为日志只是事后追溯工具,但实际上,实时监控SQL日志可以让你在攻击发生的几分钟内就发现异常。MySQL的general_log会记录所有执行的SQL语句,虽然在高并发下会带来性能开销,但对于安全敏感的业务,至少应该在应用服务器和数据库之间的代理层记录SQL日志。
更实际的做法是开启慢查询日志并设置合理的阈值,同时使用pt-query-digest等工具定期分析。注入攻击往往会触发大量结构相似但参数不同的查询,这些查询可能不会很慢,但通过日志聚合分析可以发现明显的模式异常。例如短时间内出现大量包含UNION SELECT、SLEEP()、BENCHMARK()的查询,基本可以判定为注入探测。
[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1 log_queries_not_using_indexes = 1
此外,MySQL 5.6及以上版本支持performance_schema中的events_statements_history_long表,可以查看最近执行过的SQL语句,这在应急响应时非常有用。
六、升级到MySQL 8.0并利用其安全特性
如果还在使用MySQL 5.6或5.7,强烈建议升级到8.0。MySQL 8.0在安全方面有显著增强:默认使用caching_sha2_password认证插件替代mysql_native_password,大幅提高了密码破解难度;支持角色(Role)管理,可以更精细地控制权限继承;数据字典统一存储在InnoDB中,避免了通过文件系统篡改权限表的攻击方式。
更重要的是,MySQL 8.0对SQL解析器进行了强化,修复了大量历史版本中存在的解析器绕过漏洞。很多针对5.x版本的注入绕过技巧在8.0上已经失效。升级本身就能消除一整类已知攻击面。
七、网络层隔离与传输加密
数据库端口3306绝对不应该直接暴露在公网上。即使有强密码和最小权限,直接暴露数据库端口仍然极其危险,因为MySQL的历史版本中存在多个无需认证的远程拒绝服务漏洞和缓冲区溢出漏洞。正确的做法是将MySQL绑定在内网网卡上,只允许应用服务器所在的私有网段访问,同时在网络边界防火墙或安全组中限制3306端口的入站规则。
对于跨机房或跨云的数据传输,必须启用SSL/TLS加密。MySQL支持通过ssl-ca、ssl-cert、ssl-key参数配置服务端证书,并要求客户端使用SSL连接:
[mysqld] ssl-ca = /etc/mysql/certs/ca.pem ssl-cert = /etc/mysql/certs/server-cert.pem ssl-key = /etc/mysql/certs/server-key.pem require_secure_transport = ON
设置require_secure_transport = ON后,所有非SSL连接都会被拒绝,这可以防止攻击者在同一内网中进行中间人嗅探,获取明文传输的SQL语句和查询结果。
八、定期进行注入演练与配置核查
安全配置不是一次性工作,而是需要持续验证的过程。建议每季度使用sqlmap等工具对应用进行授权渗透测试,重点验证参数化查询是否覆盖了所有输入点,以及数据库层的防护规则是否真正生效。同时建立配置基线核查清单,包括:sql_mode是否严格、FILE权限是否已回收、应用账户是否最小化、general_log是否按需开启、SSL是否强制、网络暴露面是否收敛。
将核查结果纳入变更管理流程,任何对数据库配置的修改都需要经过安全评审。很多安全事件的发生不是因为初始配置不安全,而是后续运维过程中为了方便临时放宽了限制,之后忘记恢复。
SQL注入防护从来不是单一措施能解决的,代码层的参数化查询、数据库层的严格模式和权限控制、网络层的隔离加密、以及持续的监控审计,四者缺一不可。把这些配置真正落地到生产环境中,才能让MySQL在面对注入攻击时具备足够的纵深防御能力。
