代码审计实战:自建站的入口清点与三条主线

很多站长对「代码审计」的理解停留在「买个扫描器,跑一遍,把报告里的高危改掉」。真正做过一轮完整审计的人会发现,扫描器给的是线索,不是结论;报告里最高危的那条,往往只是「输入没过滤」,而真正让站点被拖库的漏洞,藏在一条扫描器根本不认识的自研逻辑里。

这篇写的是一个小团队(一到三个人的站长)能实际跑完的代码审计流程,重点不在工具清单,而在「怎么组织、怎么判断、怎么收尾」,以及审计结果如何变成后续每次发版都能受益的东西。

一、先想清楚:自建站点的攻击面和成品软件不一样

用 WordPress、Typecho 这类成品程序的站点,漏洞主要来自插件和版本滞后,风险面是「别人写好的代码」。而自建后台、自研接口、写了几十个脚本的站点,风险面完全不同:

第一,没有第三方帮你做过安全校验。成品程序有成千上万双眼睛盯着,自研代码只有你自己。

第二,逻辑漏洞比例远高于成品程序。扫描器擅长找注入、XSS、命令执行这类有固定模式的问题,但对「这个接口忘记校验文章归属」「导出功能可以越权导出别人的数据」这类业务逻辑漏洞,误报率为零、检出率也为零。

第三,代码量小,反而好审。十几万行的项目要上 CodeQL 做数据流分析,几千行的自研站点,人工通读一遍完全可行,而且比任何工具都准。

所以站长的代码审计,正确的定位是:人工为主(覆盖逻辑漏洞),工具为辅(覆盖已知模式),配置和依赖单独审一轮。三者缺一条都会留下盲区。

二、第一步永远是资产清点,不是跑扫描器

开工前先写一张清单,列清楚:

  • 有几个入口文件(index.php、api/*.php、admin/*.php、cron 触发的脚本、接收回调的地址);
  • 哪些入口不需要登录(这是最高优先级,因为攻击者不需要任何前置条件);
  • 哪些入口能读写文件、能发命令、能连数据库;
  • 数据库账号有几种、各自的权限范围;
  • 外部依赖清单(composer.json、package.json、CDN 引入的 JS)。

清点最容易漏的是「定时任务脚本」和「后台的一次性运维脚本」。这类文件常常被丢在站点根目录下,只靠文件名不常见来当保护,一旦被搜索引擎或目录爆破工具扫到,就等于线上留了一扇没锁的门。审计第一天就该做一件事:把站点目录里所有 .php 文件按「是否经过入口路由」分类,没有经过路由的裸文件逐个确认用途,用完就删或移出 Web 根目录。

三、第二轮:人工通读的三条主线

不需要从头读到尾。按「数据从哪里进来、到哪里去、被谁允许」三条线走,效率最高。

主线一:所有输入点

把 $_GET、$_POST、$_COOKIE、$_FILES、$_SERVER['HTTP_*']、php://input 全部搜出来(grep -rn '$_GET\|$_POST\|php://input' --include=*.php),给每个输入点标三个信息:

  • 有没有做类型转换或白名单(数字用 (int),枚举用数组 in_array,路径用 realpath 前缀比对);
  • 它最终流向哪里(SQL、HTML 输出、文件路径、exec、重定向 Location);
  • 它属于哪个权限级别(游客 / 登录用户 / 管理员)。

这一步做完,一张表格就能看出全部「输入到危险汇聚点」的链路,比看扫描器报告直观得多。典型问题会在表里自己跳出来:某个接口的参数直接拼进了 file_put_contents 的路径、某个后台参数进了 header('Location: ...') 造成开放重定向、某个 cookie 值被当成 SQL 条件。

主线二:所有危险汇聚点

反向再搜一遍这几个函数:SQL 拼接(query( 里带变量)、exec/shell_exec/system/passthru/popen、include/require 带变量、eval、unserialize、file_get_contents 与 curl 的 URL 来自用户、echo 输出未转义变量。每个汇聚点确认「上游是否已经收口」。

经验上,自研站点里最常见、最容易被忽略的三类:

  • 文件包含与路径穿越:include $_GET['page'] . '.php' 这种写法,攻击者用 ../../ 就能读到服务器上的任意文件;防护写法是白名单数组,不用字符串过滤。
  • 反序列化:把 cookie 或表单里的数据直接 unserialize,等于把「对象构造」的控制权交给外部。改用 JSON。
  • 服务端请求伪造:图片采集、友链检测、webhook 这类功能会请求用户提供的 URL,攻击者用它打内网或云厂商的元数据地址。必须做「解析后的 IP 不在私网段」的校验,且要防 DNS 重绑定(先解析校验再连接,而不是校验域名)。

主线三:权限判定

把每个「需要登录」的入口列出来,检查三件事:是否在入口处就判定登录(而不是靠页面里隐藏按钮)、是否校验了资源的归属(改 id=1 为 id=2 能不能看到别人的草稿)、写操作是否校验了 CSRF。这三条是扫描器贡献最少、实际危害最大的部分。

四、第三轮:工具扫描怎么用才不浪费

人工过完主逻辑之后再跑工具,此时工具的任务变成「查漏」和「确认已知模式」:

  • 依赖漏洞:composer audit(Composer 2 已内置)和 npm audit,输出里优先看「有可用补丁且被实际调用」的条目,纯 dev 依赖可以降级处理。
  • 已知模式扫描:Semgrep 这类基于规则的工具,规则可读、误报可控,适合自研代码;PSALM、PHPStan 属于静态分析,对「类型不一致导致的隐式转换」更敏感。
  • 配置层:这部分扫描器不管,但危害常常最大 —— 关掉 display_errors(线上报错里带路径和 SQL)、关闭 allow_url_include、给上传目录加 php_flag engine off、把 .git、.env、备份压缩包挡在 Nginx 之外。

扫描器报告不要直接照单全收。处置顺序按「可达性 × 危害」排:游客可达且能写数据的最高,仅管理员可达且需要额外条件的放在最后。「不可达」的高危应当写成注释说明原因,而不是直接关掉规则 —— 半年后有人把这个函数挂到新接口上,注释就是唯一的提醒。

五、把审计结果变成可回归的资产

一次性审计的价值会随时间衰减。要让结果持续生效,做三件低成本的事:

  • 把每个确认过的问题写成一条「可复现的最小请求」(curl 命令),存进仓库的 tests/security/ 目录,下次改这块代码时手动或脚本跑一遍,确认修复没有回退;
  • 为最容易复发的一两条加 CI 检查(例如禁止在 SQL 里出现变量拼接、禁止出现 unserialize(),用 grep 级别的粗糙规则就够,比复杂规则更容易长期维护;
  • 在上传、导出、登录这三个高风险函数处加日志,记录操作者与关键参数,出事后能回溯(注意不要记录密码和完整身份证号,见下文脱敏部分)。

六、审计节奏与收尾

小站点不需要一年一次的大审计,改成「每季度一轮两小时的小审计 + 每次大改发版前半小时的定向审计」更现实:小审计覆盖入口清单和依赖漏洞,定向审计只看本次改动涉及的入口、参数和权限。

收尾时一定要写一份不超过一页的结论,包含:本轮改了哪几处、哪些是接受的残余风险及原因、下次要补哪一块。没有这份文字的审计,三个月后没人记得当初为什么放过那条高危 —— 而它很可能就是下一次事故的入口。

七、小结:审计的本质是「把不确定变成清单」

代码审计不是找漏洞比赛,而是把「我以为这里安全」换成「我确认过这里安全,理由如下」。对个人站长来说,真正可执行的路径是:先清点入口、再人工走三条主线、工具只做查漏、配置和依赖单独过、结果沉淀成可复跑的请求和规则。这套流程走两轮之后,你会发现每次发版前要看的代码越来越少,因为不确定的部分已经被清单消灭了。

Last modification:October 31st, 2026 at 07:23 pm

Leave a Comment