多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SQL注入敏感源识别与不安全SQL请求审计日志落地实践

SQL注入敏感源识别与不安全SQL请求审计日志落地实践 最近在排查一批线上接口的访问日志时我注意到不少SQL注入探测请求其实并没有被WAF挡住而是直接打到了应用层被数据库“正常”执行了。说来也怪系统本身没有出大事故但那种“知道有问题却找不到证据”的感觉特别难受。于是我就做了这么一件事把SQL注入相关的敏感源全部识别出来并且把每一条不安全的SQL请求完整记录到独立的审计日志里。这套东西做下来给我的直接价值有三点第一能够还原每次可疑请求的完整上下文包括原始参数、拼接后的SQL、执行的接口和IP第二能够在WAF和数据库之间补一层应用层审计专门用来发现那些“没有触发拦截但明显不该出现”的请求第三把误报和漏报的数据喂回给规则引擎持续优化检测策略。这篇就用我实际落地的方案把“如何处理SQL注入敏感源”和“如何记录所有不安全的SQL请求”这两件事拆开来讲。如果你也正在做安全审计、接口治理或者漏洞排查这篇文章应该能给你一套可以直接抄作业的思路。1. 项目拆解什么才叫“SQL注入敏感源”标题里的“敏感源”这个词很多人第一反应是“数据库里的敏感字段”。其实这里说的敏感源指的是Web应用里一切能被用户控制、最终可能流入SQL语句的输入源头。理解清楚这个概念后面的所有设计才立得住。1.1 三个容易混淆的概念先分清楚三个东西SQL注入、敏感源、不安全的SQL请求。它们之间有递进关系但很多人常常混在一起来讲。SQL注入是一种攻击手法攻击者通过修改请求参数把额外的SQL代码塞进应用原本的查询逻辑里。敏感源指的是“攻击者的输入入口”包括URL中的查询参数、POST表单字段、JSON报文中的字段、Cookie、请求头甚至上传文件名。不安全的SQL请求指的是最终被拼接到SQL语句中执行的那条完整请求它可能成功也可能失败但只要它携带了可疑的注入特征或者来源参数被直接拼接进了SQL它就属于“不安全请求”。用一个登录功能来举例就比较直观了。假设接口里面有一行代码$sql SELECT * FROM users WHERE username . $_POST[username] . AND password . md5($_POST[password]) . ;这里$_POST[username]和$_POST[password]就是敏感源它们由用户完全控制。拼出来的$sql就是一条不安全的SQL请求。如果用户传入admin or 11最终执行的SQL就变成了SELECT * FROM users WHERE username admin or 11 -- ...。所以处理敏感源的核心动作就是对所有可能与SQL语句产生拼接的输入做识别、记录和校验。1.2 为什么“记录”比“拦截”更难通常大家优先想到的是“拦截”但真正做安全的人都知道拦截只是第一步记录才是更麻烦的事情。拦截是一个二元操作命中规则就拒绝未命中就放行。但记录不一样它要在不影响业务的前提下把海量请求里的可疑项捞出来而且要保留足够多的上下文方便事后追溯。记录需要回答的问题包括这个请求是谁发出来的、用了什么参数、拼出了什么样的SQL、这条SQL是否真的执行了、执行结果如何。光是这些信息的采集和整理就比写一个过滤器复杂得多。另一个现实的原因是WAF和网关通常只记录“命中规则并被拦截”的请求那些没被拦截但实际包含注入特征的请求往往会直接消失。我在实际排查中就遇到过一种情况攻击者在同一个参数里反复尝试了二十几种不同的注入语句WAF一条也没拦住因为规则库里没有覆盖这种变体。如果应用层不记录SQL请求这类探测行为就完全不可见了。所以记录的功能定位不是替代WAF而是补上应用层审计这块空白。2. 整体方案设计从接入点到存储的完整链路记录所有不安全的SQL请求并不只是在日志里多打几行字那么简单。要把这件事做成可用的系统需要把接入点、检测规则、日志格式、存储方式全部串起来。2.1 协议层采集 vs 应用层埋点 vs 数据库层审计接入方式我对比过三种它们各自覆盖的维度完全不同实际项目中最好组合使用但如果只能选一个我推荐以应用层埋点为主。采集层实现方式能看到原始参数能看到最终SQL能看到执行结果接入成本协议层OpenResty、API网关、WAF插件可以不行不行中应用层中间件、拦截器、ORM Hook可以可以部分可以低数据库层通用日志、Audit插件、Binlog解析不行可以可以高协议层的好处是统一接入对所有服务生效不需要改动业务代码。但它只能看到HTTP请求无法知道应用最终往数据库发送了什么SQL。数据库层能看到真实执行的SQL但看不到原始请求参数而且在高并发下开启通用日志或者全量审计的代价不小容易造成磁盘IO飙升和性能回退。应用层埋点是一个折中方案也是我最终采用的方案。它在请求入口处采集参数在SQL执行层获取最终语句两边信息都能拿到。更关键的是应用层还能拿到业务上下文比如当前登录用户ID、请求路由、模块名称这些信息对定位问题非常有用。2.2 方案选型用轻量级拦截器实现安全审计我的技术栈以PHP为主部分服务用的是Java Spring Boot所以设计上尽量跟语言无关。整体思路是在框架的请求生命周期里注册一个全局拦截器它负责做三件事收集敏感源参数、用规则引擎判断参数是否可疑、把可疑参数连同最终SQL写入审计日志。模块拆分成四块敏感源采集器统一从HTTP请求中抽取参数覆盖GET、POST、Header、Cookie并递归解析JSON。规则判定引擎基于正则特征库做匹配每个规则有风险等级和默认动作分为“仅记录”“记录并告警”“记录并拦截”三等。SQL语句采集器在ORM或者数据库执行封装层拦截query()、exec()这类方法把实际执行的SQL快照取回来。审计日志服务将请求信息、参数快照、规则命中结果、SQL快照组合成一条结构化日志异步写入独立的审计表或日志文件。这样做的好处是不需要对每个业务接口单独改代码只需要在框架核心中注册一次拦截器后面新上线的接口就自动纳入审计范围。我实际执行下来一个中等复杂度的项目大概只需新增两个核心类和一个配置文件侵入性比预想中低很多。3. 核心实现规则引擎与日志落地细节规则引擎是整个系统的检测中枢日志落地的设计则决定了这些记录将来能用出什么价值。这两块我分别讲一下具体做法。3.1 基础特征规则的取舍特征规则的来源主要有两类一类是网上公开的注入Payload特征另一类是从线上恶意请求里提取的高频特征。规则不能贪多重点是覆盖攻击者最常用的手法同时控制误报率。我维护了一份精简但覆盖较广的规则表字段包括规则ID、规则名称、匹配正则、风险等级、处置动作。核心规则大致如下规则ID规则名称正则特征风险等级默认动作R001联合查询注入union\s(?:all\s)?select高记录并告警R002布尔盲注\b(andor)\b\s\d\s*[!]中R003时间盲注sleep\s*\(或benchmark\s*\(高记录并告警R004报错注入updatexml\s*(extractvalue\s*(高R005万能密码\s*(oror)\s*?\s*\s*中R006注释符绕过/\*.*\*/或 --\s#中R007内联注释/\*![0-9]{5,}.*\*/高记录并告警R008堆叠注入;\s*(selectinsertupdate正则语言写成PHP或者Java的Pattern都可以关键是要预编译不能每次请求都重新编译一遍规则。以PHP为例我在初始化阶段用数组保存所有规则的编译结果private array $compiledRules []; private function compileRules(): void { foreach ($this-rules as $rule) { $this-compiledRules[$rule[id]] [ name $rule[name], pattern / . $rule[pattern] . /i, risk $rule[risk], action $rule[action], ]; } }这里有一个取舍问题规则过于宽泛会带来误报比如“or 11”这种特征在搜索参数里很容易被触发规则过于严格又会漏报比如攻击者把union select写成union/**/select就能绕过单条正则。所以我的做法是先用基础正则筛出候选再对高怀疑度的参数走一次分值计算。每个命中规则根据等级加分总分超过阈值才升级为“告警”甚至“拦截”。3.2 敏感源参数的归一化处理参数在到达应用层之前会经历多次编码攻击者最喜欢利用这一点来绕过检测。我见过最常见的变体包括URL编码、双重URL编码、Unicode编码、大小写混淆、用加号代替空格、用注释符拆分关键字。归一化处理的思路是“解码到合理深度而不是无脑多次解码”。因为如果对参数反复解码正常业务里的合法内容也可能被解出注入特征造成误报。我采用的做法是对每个参数值做一次urldecode再做一次大小写归一化最后把常见的编码替代符还原为等价字符。注意保留原始值解码后的值只用于规则匹配不用于业务逻辑。下面这段是PHP的归一化示例private function normalizeParam(string $value): string { $decoded rawurldecode($value); $lower strtolower($decoded); $normalized str_replace( [/**/, /*, */, --, #], [ , , , , ], $lower ); return $normalized; }这里把注释符替换成空格是因为很多绕过手法用注释来拼接关键字比如uni/**/on sel/**/ect。替换成空格后规则正则仍然能匹配到union select模式。加号处理成空格放在SQL拼接层处理不要在这一步盲目替换否则时间参数里的加号会被破坏。3.3 审计日志表结构与输出格式审计日志如果只是输出到普通日志文件检索起来非常痛苦。我设计了一张独立的审计表字段尽量覆盖事件追溯的各个维度。建表语句如下CREATE TABLE sql_audit_log ( id bigint unsigned NOT NULL AUTO_INCREMENT, occur_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, project_name varchar(64) NOT NULL DEFAULT , app_route varchar(255) NOT NULL DEFAULT , http_method varchar(10) NOT NULL DEFAULT , request_uri varchar(512) NOT NULL DEFAULT , remote_ip varchar(64) NOT NULL DEFAULT , user_agent varchar(512) NOT NULL DEFAULT , user_id varchar(64) NOT NULL DEFAULT , matched_rules varchar(512) NOT NULL DEFAULT , risk_score int NOT NULL DEFAULT 0, risk_level varchar(10) NOT NULL DEFAULT low, action_taken varchar(10) NOT NULL DEFAULT log, full_sql text, raw_params json DEFAULT NULL, exec_state varchar(20) NOT NULL DEFAULT unknown, stack_trace text, PRIMARY KEY (id), KEY idx_occur_time (occur_time), KEY idx_remote_ip (remote_ip), KEY idx_rule (matched_rules) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;raw_params用JSON类型存储可以直接在MySQL里用函数检索某个参数是否命中规则。full_sql保存的是脱敏后的SQL快照不能直接把带完整数据的SQL扔进日志而是要把字符串字面量替换成?防止敏感数据落盘。落盘格式我统一用了一行JSON方便接入日志采集系统。实际输出类似这样{time:2025-01-18 14:23:01,route:/user/search,method:GET,uri:/user/search?keyword1%27%20or%201%271,ip:203.0.113.7,rules:[R002],score:2,level:medium,action:log,full_sql:SELECT * FROM users WHERE username LIKE ?,params:{keyword:1 or 11}}3.4 执行状态回采让审计日志更有说服力只记录“可疑请求”还不够还需要知道这条SQL最终执行成功没有。我通过封装数据库执行方法把SQL的执行状态回填到日志中。状态分为三类blocked请求被规则直接拦截SQL未执行。exec_failed请求放行但SQL执行抛出了异常。exec_success请求放行SQL正常执行。exec_failed是我最关注的。它往往说明攻击者的语句被数据库解析过虽然没有成功返回数据但已经触发了部分解析逻辑。这类日志对评估漏洞利用难度非常有价值。实现上我是在数据库访问类的query()方法里加了一个全局钩子在SQL执行前后记录指纹并回填结果具体代码后面实操部分会展示。4. 实操过程从零搭建“不安全SQL请求记录系统”下面进入可以直接复现的环节。我以PHP 8 ThinkPHP框架为例但思路可以平移到任何框架。如果你用的是Java Spring Boot核心逻辑完全一样只是拦截器的实现方式不同。4.1 需要准备的环境PHP 8.0 以上开启pdo_mysql扩展。MySQL 5.7 以上建议使用8.0以便支持JSON字段。一个已有的Web接口项目或者本地搭建一个DVWA、Pikachu靶场用来测试。日志存储目录独立于业务日志避免被日志轮转冲刷掉。环境的唯一硬性要求是项目必须在数据库层和请求层之间有统一的入口。如果项目里到处都是mysqli_query()散弹式调用建议先封装一个统一的数据库类否则无法采集SQL快照。4.2 核心代码拦截器、规则引擎、日志写入器我按功能拆分几个类文件实际部署时按项目规范放到对应目录即可。首先是请求敏感源采集器它负责从超全局变量中提取参数并递归解析POST过来的JSON体class SensitiveSourceCollector { public function collect(): array { return [ get $_GET, post $this-extractPostParams(), cookie $_COOKIE, header $this-extractSensitiveHeaders(), ]; } private function extractPostParams(): array { $contentType $_SERVER[CONTENT_TYPE] ?? ; if (stripos($contentType, application/json) ! false) { $rawBody file_get_contents(php://input); $decoded json_decode($rawBody, true); if (is_array($decoded)) { return $this-flattenArray($decoded); } } return $_POST; } private function flattenArray(array $input, string $prefix ): array { $result []; foreach ($input as $key $value) { $fullKey $prefix ? (string)$key : $prefix . . . $key; if (is_array($value)) { $result array_merge($result, $this-flattenArray($value, $fullKey)); } else { $result[$fullKey] $value; } } return $result; } private function extractSensitiveHeaders(): array { $headers []; foreach ([User-Agent, X-Forwarded-For, Referer, Authorization] as $name) { $key HTTP_ . strtoupper(str_replace(-, _, $name)); if (!empty($_SERVER[$key])) { $headers[$name] $_SERVER[$key]; } } return $headers; } }这里比较关键的是flattenArray()方法。很多接口的POST参数是嵌套JSON攻击者会把注入语句藏在某个嵌套层级里。如果不递归打平只检查第一层漏报率会非常高。我实际测试中发现只要把参数的key加上路径前缀打平比如user.address.detail再去做正则匹配嵌套结构的检出率立刻上来了。然后是规则引擎负责对采集到的参数值做归一化和特征匹配class SqlInjectionRuleEngine { private array $compiledRules []; private array $rules [ [id R001, name union_select, pattern union\s(?:all\s)?select, risk high, score 3], [id R002, name boolean_blind, pattern \b(?:and|or)\b\s\d\s*[!], risk medium, score 2], [id R003, name time_blind, pattern sleep\s*\(|benchmark\s*\(, risk high, score 3], [id R004, name error_based, pattern updatexml\s*\(|extractvalue\s*\(, risk high, score 3], [id R005, name comment_bypass, pattern /\*.*?\*/, risk medium, score 1], ]; public function __construct() { foreach ($this-rules as $rule) { $this-compiledRules[$rule[id]] [ name $rule[name], pattern / . $rule[pattern] . /i, risk $rule[risk], score $rule[score], ]; } } public function checkParams(array $params, callable $normalizeFunc): array { $matched []; foreach ($params as $key $value) { if (!is_string($value)) { continue; } $normalized $normalizeFunc($value); foreach ($this-compiledRules as $ruleId $rule) { if (preg_match($rule[pattern], $normalized)) { $matched[$ruleId] $rule; $matched[$ruleId][param] $key; $matched[$ruleId][original] $value; } } } return $matched; } }这套设计的优势在于规则引擎不依赖具体框架数据结构也只是纯数组可以很轻松地移植到Java或者其他语言。$this-rules也可以改从配置文件读取方便运营人员维护规则而不用改代码。再往下是审计日志写入器。注意写入要异步化不要因为记录日志拖慢用户请求。PHP里最简单的做法是写入内存缓冲再用register_shutdown_function做一次性落库Java项目则可以使用线程池或者消息队列。class AuditLogWriter { private PDO $pdo; public function __construct(PDO $pdo) { $this-pdo $pdo; } public function write(array $logData): bool { $sql INSERT INTO sql_audit_log (project_name, app_route, http_method, request_uri, remote_ip, user_agent, user_id, matched_rules, risk_score, risk_level, action_taken, full_sql, raw_params, exec_state, stack_trace) VALUES (:project_name, :app_route, :http_method, :request_uri, :remote_ip, :user_agent, :user_id, :matched_rules, :risk_score, :risk_level, :action_taken, :full_sql, :raw_params, :exec_state, :stack_trace); $stmt $this-pdo-prepare($sql); return $stmt-execute([ :project_name $logData[project_name] ?? , :app_route $logData[app_route] ?? , :http_method $logData[http_method] ?? , :request_uri $logData[request_uri] ?? , :remote_ip $logData[remote_ip] ?? , :user_agent $logData[user_agent] ?? , :user_id $logData[user_id] ?? , :matched_rules json_encode($logData[matched_rules] ?? []), :risk_score $logData[risk_score] ?? 0, :risk_level $logData[risk_level] ?? low, :action_taken $logData[action_taken] ?? log, :full_sql $logData[full_sql] ?? , :raw_params json_encode($logData[raw_params] ?? []), :exec_state $logData[exec_state] ?? unknown, :stack_trace $logData[stack_trace] ?? , ]); } }在write()方法里我把matched_rules和raw_params都转成JSON字符串存储保留字段的灵活性。如果以后要加字段不需要每次修订表结构。4.3 SQL执行层钩子为了采集完整SQL快照和执行状态我在数据库查询封装层加了一段钩子代码。以PDO封装类为例class SafePdo { private PDO $pdo; private AuditLogWriter $auditWriter; public function query(string $sql, array $params []): PDOStatement { $startTime microtime(true); try { $stmt $this-pdo-prepare($sql); $stmt-execute($params); $state exec_success; return $stmt; } catch (Throwable $e) { $state exec_failed; $this-recordExecLog($sql, $params, $state, $e-getMessage()); throw $e; } finally { $execTimeMs (microtime(true) - $startTime) * 1000; if ($execTimeMs 500) { $this-recordExecLog($sql, $params, slow_query, ); } } } private function recordExecLog(string $sql, array $params, string $state, string $errorMsg): void { $maskedSql $this-maskSqlLiteral($sql); $this-auditWriter-write([ app_route $_SERVER[REQUEST_URI] ?? , full_sql $maskedSql, exec_state $state, matched_rules [], risk_score 0, risk_level info, action_taken log, raw_params $params, stack_trace $errorMsg, ]); } private function maskSqlLiteral(string $sql): string { return preg_replace(/[^]*/, ?, $sql); } }这里有两个细节值得展开。第一maskSqlLiteral()的作用是把SQL中的字符字面量替换成占位符避免用户提交的具体参数值落盘。第二finally块里对执行时间超过500毫秒的SQL也做了一次记录这条不是注入检测但对发现慢SQL和潜在的延时注入很有帮助。时间盲注的显著特征就是单条SQL执行时间异常把慢查询和规则引擎的流量关联起来看更容易定位问题。4.4 给拦截器加上总控开关把上面的类组装起来放进框架的入口中间件里。总控设计成三个等级off完全关闭检测不影响线上。log_only只记录不拦截用于灰度观察。enforce根据规则动作执行高风险规则拦截中低风险记录。用配置文件控制模式不要每次改代码。PHP里的实现大致长这样class SqlAuditMiddleware { private string $mode log_only; public function handle($request, $next) { if ($this-mode off) { return $next($request); } $collector new SensitiveSourceCollector(); $params $collector-collect(); $engine new SqlInjectionRuleEngine(); $matched $engine-checkParams($params, function ($value) { $decoded rawurldecode($value); return preg_replace(~/\*.*?\*/|--|#~, , strtolower($decoded)); }); if (!empty($matched)) { $this-recordAndMaybeBlock($request, $params, $matched); } return $next($request); } }在recordAndMaybeBlock()里我计算总分根据阈值决定是否直接返回403。总分的计算策略是每条命中规则按分值加总同一参数命中多条规则额外加1分不同参数命中同一规则不重复加分。这个策略比简单计数更贴近实际情况。4.5 在靶场环境中的验证结果我在本机分别用DVWA的low级别和Pikachu靶场做了验证确保规则引擎能全程记录所有不安全的SQL请求。先看DVWA的SQL注入模块。在输入框提交1 or 11后日志里完整记录了一条R002规则的命中记录参数关键字是id可以看出对应的SQL被拼接上了额外条件。提交1 union select 1,2,3--时R001和R006同时命中返回403日志里action_takenblocked。这两次请求的完整URI、UA和IP都记录在案。再看Pikachu的搜索型注入。在搜索框提交kobe and sleep(3)--时R003规则命中。由于时间盲注默认动作是“记录并告警”请求被放行但SQL执行时间超过3秒慢查询记录同时生成。把这两条记录放在一起基本可以确认攻击者在尝试延时注入。测试中我还发现一个情况搜索型接口本身需要接收包含单引号的内容做模糊匹配比如用户名“OBrien”会被规则R005误判为万能密码。这就引出了下一节要讲的问题如何控制误报同时堵住漏报。5. 误报与漏报的处理技巧真正上生产环境之后最头疼的往往不是规则不够多而是误报太多、漏报隐蔽。我把实际踩过的坑和相应的对策整理成一个小节。5.1 误报重灾区搜索框、排序字段、统计报表误报最高的几类参数集中在搜索关键词、排序字段、报表查询条件。搜索框输入or、and、#的场景太常见了如果规则不加限制地全量匹配误报率会高到没人愿意看日志。针对装箱参数我建议先按参数类型分流。数值型参数也就是内容纯数字或固定白名单枚举的参数直接做强类型校验不跑正则规则。比如id、page、limit如果非预期类型的输入出现反而需要特别记录。字符串型参数只有包含SQL关键字时才进入规则匹配但这还不够因为or是一个合法英文单词出现在搜索词里十分正常。我的处理方案是引入“词边界”和“上下文约束”。比如规则R002从\bor\b升级为\bor\b\s\d\s*[!]这样or后面必须跟数字和比较符才触发大幅降低了英文单词的误伤概率。再比如搜索参数给它单独维护一份白名单只要请求命中了搜索路由R002的等级降为“仅记录”只有当多个规则同时命中时才升级告警。5.2 漏报的三种隐蔽场景漏报比误报更危险因为攻击成功了你还不知道。根据我的测试漏报主要来自三个方面。第一种是存储型注入。攻击者的payload先通过接口参数正常入库之后在另一个页面被取出来拼进SQL执行。这种情况下请求阶段看不到任何可疑参数规则引擎当然发现不了。唯一的应对办法就是数据库层审计以及应用层在SQL执行时检测传入参数里的“数据流特征”。我现在做的是对从数据库读出来的字段值也做一次规则快检虽然不能完全防御但能在二次查询时抓到线索。第二种是POST JSON体里的嵌套字段。前面提到用flattenArray()打平处理但还有一个细节容易漏JSON字段的key本身也可能被注入比如{select:1}。所以不仅value要检测key也要跑一遍规则。第三种是编码绕过。正则只能匹配归一化后的内容而归一化只做了一层解码。如果攻击者做双重编码比如%2575%256e%2569%256f%256e解码一次后变成%75%6e%69%6f%6e看起来是正常字符串但如果再做一次urldecode就会还原成union。我没有无脑解码两层是因为这会造成大量误报。折中办法是第一次解码没有命中任何规则时再做第二次解码并重新匹配且注明“double_encode”标签方便人工确认。5.3 误拦截导致线上故障时的应急方案最怕的一种事故是规则太严把正常用户的某个请求拦截了结果影响线上业务。这种事一旦发生第一要务不是修规则而是恢复服务。我的做法是给审计系统预留一个“熔断开关”。当收到告警、发现误拦截时可以在配置中心把mode从enforce降为log_only规则引擎立即停止拦截只做记录。降级后所有原本要拦截的请求都会放行但依然会留下审计日志为后续分析保留证据。等确认规则无误后再逐步恢复enforce模式。应急流程建议写进操作手册而不是临时翻代码。我给自己定的流程是收到误报工单 - 降低模式为log_only - 用日志里的数据复现误报场景 - 修改或下线对应规则 - 在测试环境验证 - 重新切换到enforce模式 - 观察24小时。这套流程下来线上受影响的时间能控制在十分钟以内。5.4 性能开销与写入优化不少人担心加一层审计会影响接口性能这个顾虑合理。我在实际压力测试中把采集、匹配、写入全开时接口延迟平均增加约8%到12%。这个数字还在接受范围但优化空间也是有的。优先做的三件事正则预编译、参数短路、异步写入。正则预编译就是规则引擎初始化时提前编译Pattern避免每次请求现场编译。参数短路则是利用“危险参数黑名单 参数名白名单”的组合像password、token这种本身就可能含特殊字符的字段先做长度阈值判断太短直接跳过规则。异步写入用队列方案请求线程只负责把日志推到内存队列后台进程批量落库这样IO阻塞基本消失。再补充一点对日志表的查询要控制好。审计表的增长非常快通常一天就有几十万条全表查询会拖垮数据库。我的建议是审计表按周分区超过30天的数据归档到冷存储日常只保留最近两周的数据用于检索。如果一定要全文检索日志内容考虑接入ELK不要直接在MySQL里跑LIKE查询。6. 一块容易被忽略的拼图规则灰度与运营反馈很多人以为把代码写完了、日志能记录了任务就算结束了。其实真正让这套系统持续发挥价值的是后续的规则运营。如果不迭代规则库很快就会过时攻击者的手法一变日志就再也捞不到新东西了。我把运营动作固定为每周一次的复盘导出上周命中记录的TOP10参数和要求手工比对一遍同时检查是否有明显异常但在规则库里没有命中的请求提取新的特征入库。这个机制运行了一个月之后规则库从最初的12条扩展到了25条误报率反而比最初更低了因为新增规则都经过了实际样本验证针对性更强。另外这套“记录所有不安全的SQL请求”的系统不要只服务于安全部门。品类运营、开发排查问题、甚至产品经理看用户行为时都会遇到“某个可疑请求导致数据异常”的场景。我把审计日志中已经脱敏的访问来源、路由、风险等级数据做成一个简易的检索引擎放在内网运维平台里支持按IP、时间、规则ID、执行状态组合筛选。这样一来运营和研发都能自助查询安全人员也能把精力集中在处理高危问题上。我个人的体会是SQL注入的防护没有银弹WAF、数据库审计、应用层拦截各管一段。而“记录所有不安全的SQL请求”这个动作恰好是把三段串联起来的一根线。真正跑一段时间之后你会发现它的价值不在于拦住多少次攻击而在于当你有疑问时能把当时的现场完全还原出来。有了这套记录分析和改进就有了依据安全也就从“碰运气”变成了“可迭代”。
返回列表