数据库突然CPU飙升到100%,连接池瞬间耗尽,业务系统全线崩溃,运维群警报响个不停。这不是什么罕见场景,绝大多数时候,元凶不是硬件故障,而是一段被恶意构造的SQL语句,或者一波自动化爬虫的异常高频请求。问题摆在这里,怎么快速识别出来,又怎么有效拦截?下面直接讲实操方法。
从慢查询日志里挖出异常指纹很多人把慢查询日志当成性能调优工具,其实它是最直接的安全侦察兵。正常业务SQL的执行模式是有规律的,凌晨三点突然出现大批量全表扫描的SELECT,或者从未出现过的复杂嵌套查询,这就是明显信号。先开启数据库的慢查询日志,设置合理的阈值,比如MySQL里long_query_time设为1秒,然后配合pt-query-digest这类工具做聚合分析。
具体命令不复杂:
SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; SET GLOBAL log_queries_not_using_indexes = 'ON';
分析的时候重点关注几个指标:执行次数突增的SQL模板、扫描行数与返回行数比例严重失调的查询、以及包含异常函数调用的语句。比如突然出现大量benchmark()、sleep()、load_file()这类函数,基本可以确定是SQL注入探测行为。把这类SQL模板提取出来,直接加入应用层的黑名单规则里,第一时间阻断后续攻击。
实时流量抓包与协议解析慢查询日志是事后追溯,要实时发现异常,必须在流量入口做文章。在数据库前面部署一层流量镜像或者代理,比如使用ProxySQL、MaxScale这类中间件,把所有发往数据库的请求完整记录下来。关键是要解析MySQL或PostgreSQL的协议包,提取出SQL语句、执行时间、来源IP、连接用户这些元数据。
实操中可以用tcpdump抓包配合Wireshark分析,但生产环境更推荐直接用数据库中间件的审计插件。以ProxySQL为例,开启查询日志功能:
SET mysql-eventslog_filename = 'queries.log'; SET mysql-eventslog_default_log = 1; SET mysql-eventslog_format = 2; LOAD MYSQL VARIABLES TO RUNTIME; SAVE MYSQL VARIABLES TO DISK;
这样就能拿到每一条SQL的完整记录。接下来写一个简单的分析脚本,统计每个来源IP的查询频率、每次查询返回的数据量、以及SQL语句的结构特征。正常业务请求的查询频率是平稳的,单个IP每秒几十次查询算正常,但突然飙到每秒几百上千次,而且查询模式高度重复,这就是典型的爬虫或者CC攻击行为。发现这类IP后,直接在中间件层面设置黑名单规则,拒绝连接。
基于SQL语法树的异常检测单纯靠频率和流量特征还不够,真正危险的SQL注入攻击往往请求量很小,伪装成正常查询,但SQL语句的语法结构存在恶意特征。这时候需要深入解析SQL语法树。正常的业务SQL,WHERE条件里的逻辑结构是固定的,比如用户登录查询总是“SELECT * FROM users WHERE username=? AND password=?”,但注入攻击会改变这个结构,出现“OR 1=1”、“UNION SELECT”这类异常分支。
实现方法是在中间件层嵌入SQL解析器,比如用Golang的vitess-sqlparser或者Python的sqlparse库,把每条SQL解析成AST语法树。然后定义正常业务SQL的结构白名单,凡是结构不匹配的查询直接拦截。更进一步,可以检测SQL语句中是否包含敏感操作组合,比如同时出现SELECT和INTO OUTFILE,或者包含对系统表的查询。这类检测规则写起来不复杂:
import sqlparse
def detect_anomaly(sql):
parsed = sqlparse.parse(sql)
for stmt in parsed:
tokens = [str(t) for t in stmt.flatten()]
# 检测UNION SELECT注入
if 'UNION' in tokens and 'SELECT' in tokens:
return True
# 检测写文件操作
if 'INTO' in tokens and 'OUTFILE' in tokens:
return True
# 检测系统表查询
if any('information_schema' in t.lower() for t in tokens):
return True
return False
这套机制部署在数据库代理层,对业务代码零侵入,而且检测粒度比简单的正则匹配精准得多,误报率能控制在很低水平。
异常连接行为与账号风险识别数据库安全不只是SQL注入,账号滥用和权限提升同样致命。很多团队习惯用同一个数据库账号连接所有服务,一旦某个微服务被攻破,攻击者就能用这个账号为所欲为。识别这类风险,需要监控每个数据库账号的连接行为和权限使用情况。
首先检查账号的权限配置,执行以下查询找出拥有过高权限的账号:
SELECT user, host,
SUM(Select_priv = 'Y') as has_select,
SUM(Insert_priv = 'Y') as has_insert,
SUM(Update_priv = 'Y') as has_update,
SUM(Delete_priv = 'Y') as has_delete,
SUM(Grant_priv = 'Y') as has_grant
FROM mysql.user
GROUP BY user, host;
然后监控每个账号的实际操作行为。一个只应该读取配置表的账号,突然执行了DROP或ALTER操作,这就是明显的异常。在MySQL 5.7及以上版本,可以启用审计插件记录所有操作,或者通过general_log临时开启全量记录来排查问题。长期方案是在数据库代理层记录每个账号执行过的SQL类型分布,建立正常行为基线,一旦偏离基线就触发告警。
另外要关注连接来源的变化。一个账号平时只从内网特定网段连接,突然从外部IP或者非业务服务器发起连接,极可能是凭据泄露。通过ProxySQL的stats_mysql_processlist表可以实时查看所有连接的来源IP和状态,写个定时任务检查异常连接:
SELECT srv_host, user, COUNT(*) as conn_count FROM stats_mysql_processlist GROUP BY srv_host, user HAVING conn_count > 50;
单个IP的连接数异常升高,要么是应用服务器配置错误,要么是恶意连接池耗尽攻击,都需要立刻处理。
主动防御:动态规则与自动拦截识别出异常只是第一步,自动拦截才是关键。手动封IP太慢,等运维看到告警的时候数据库可能已经挂了。需要在数据库代理层建立动态规则引擎,根据实时分析结果自动下发拦截策略。
规则引擎的设计思路是分层触发:第一层是频率限制,每个来源IP每秒最大查询数设一个阈值,超过就自动加入临时黑名单,冷却时间五分钟。第二层是SQL结构黑名单,凡是命中注入特征规则的查询,直接断开连接并封禁来源IP一小时。第三层是账号行为限制,检测到高危操作比如DROP DATABASE、TRUNCATE TABLE时,如果不是在运维窗口时间内执行,直接拒绝并锁定账号。
以ProxySQL为例,可以通过mysql_query_rules表动态添加拦截规则:
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, error_msg, apply) VALUES (100, 1, '.*UNION.*SELECT.*', 'SQL injection detected', 1); LOAD MYSQL QUERY RULES TO RUNTIME; SAVE MYSQL QUERY RULES TO DISK;
这套规则可以跟自动化脚本联动,分析程序发现异常SQL模板后,调用ProxySQL的管理接口自动写入拦截规则,整个响应时间控制在秒级。同时要把拦截事件推送到告警通道,通知DBA做人工复核,避免误杀正常业务。
数据备份与事后溯源拦截住了攻击不代表事情结束,完整的数据库安全体系必须包含事后溯源能力。所有被拦截的异常请求,原始SQL语句、来源IP、时间戳、使用的数据库账号这些信息要完整保留,存入独立的安全审计库。一旦后续发现数据被篡改或泄露,就能快速定位到具体的攻击时间点和操作内容。
溯源数据的存储要跟生产数据库物理隔离,防止攻击者清理痕迹。可以用ClickHouse或者Elasticsearch这类时序数据库存储审计日志,查询效率高,而且支持大规模数据聚合分析。定期对审计数据做离线分析,找出长期潜伏的慢速攻击或者低频数据泄露行为,这类攻击单次请求看起来完全正常,只有把时间跨度拉到几周甚至几个月,才能看出数据被一点点拖走的痕迹。
最后强调一点,所有的拦截规则和检测模型都需要持续迭代。攻击手法在进化,业务系统也在变化,今天正常的查询模式,明天可能就变成了风险信号。建立定期复盘机制,每周分析一次拦截记录和误报情况,调整规则阈值,补充新的检测特征。数据库安全不是一次性工程,而是一个持续对抗的过程。
