一次「备份文件被公网下载」的排查

◷2026 年 10 月 15 日 ◈服务器 ✎460 字

站点跑了几个月,访问一切正常,直到有人在日志里看到一条奇怪的请求:

GET /srvcard/btapi_configured.php.bak-dual-20261007 200 9828

问题出在哪

nginx 的 PHP 规则写的是:

location ~ \.php$ {
    fastcgi_pass unix:/tmp/php-cgi-82.sock;
}

它只匹配以 .php 结尾的文件。btapi_configured.php.bak-dual-20261007 不满足这个条件,于是落到了 location / 的静态文件处理分支——nginx 把它当成普通文本,原样返回。

更糟的是,PHP 文件里通常写着配置:

面板 API 密钥、数据库账号口令、第三方 token,全都在这类文件里。

怎么确认范围

不要只看一个链接。按后缀扫一遍 webroot:

find /var/www /www/wwwroot -type f \
  \( -name '*.bak*' -o -name '*.sql' -o -name '*.db' \
     -o -name '*.old' -o -name '*.swp' -o -name '*~' \) | wc -l

结果 47 个。全部可下载。

处置顺序

  1. 先把文件移出 webroot,不要就地删——保留目录结构放进 /root/webroot-quarantine-<日期>/,随时能还原。
  2. 再补一条全局规则,防止下次再犯:
location ~* \.(bak|old|orig|save|swp|tmp|sql|db|sqlite3?|log|lock|inc|cnf|ini|dist|example)([._-][^/]*)?$ {
    deny all;
}
  1. 最后轮换所有曾被暴露的密钥。文件不可下载了,不代表密钥还安全——它已经在公网待了很久。

两个容易忽略的坑

  • 默认站(server_name _)往往没人维护。如果它的 location ~ \.php$ 被注释掉了,任何未匹配的域名都会命中它,PHP 源码直接明文输出。
  • 正则里的 ~* 优先级高于普通前缀 location,但低于 ^~。所以 /srvcard/ 这种 ^~ 前缀不会被覆盖,需要单独确认。

收尾验证用不存在的路径探针最直观:请求 /__probe.bak,返回 403 才说明规则真的生效;返回 404 说明这个 server 块根本没配上。