≡articles //snippets ./categories $uses ~about /search

网站目录里的 .bak 备份能被任何人下载:一台服务器 4 个站清出 250 多个

先说结果:

  • 一台宝塔服务器上跑着 4 个站。查下来,网站目录里有 250 多个备份文件,外网都能直接下载,访问返回 200。光一个站就有 161 个。
  • 来源很普通:改配置的流程里写了「改之前先备份,文件名带 bak-日期」,没写备份放哪,备份就都放在了原文件旁边,也就是网站目录里。
  • 最危险的是 .php.bak。Nginx 只把 .php 结尾的请求交给 PHP 执行,.php.bak 会被当成普通文件原样返回,源码直接能看到。这次有一个备份里带着一串密钥,好在已经停用了。
  • 处理顺序:挪出网站目录(不删)→ 清 Cloudflare 缓存 → Nginx 加一条兜底规则,像备份的文件名一律 404。
  • 11 月 4 日复查:所有站的网站目录里 0 个备份文件;放一个测试用的 .bak 文件进去,外网返回 404。

一、怎么发现的

起因是给服务器做一次权限检查,顺手列了一下网站目录里的文件,看到一串这样的名字(文件名改过,格式一样):

robots.txt.bak-20261018-033813
mu-plugins/xxx.php.bak-20261006-041919
sitemap.xml.bak-20261012

拿 curl 从外面试了几个:

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/robots.txt.bak-20261018-033813
200

全是 200。主题目录里的模板备份、mu-plugins 里的插件备份,都能直接下载。

二、为什么 .php.bak 比原文件还危险

原文件 xxx.php 被访问时,Nginx 把它交给 PHP-FPM 执行,访客看到的是执行结果,看不到源码。宝塔给 WordPress 站点生成的配置里,交给 PHP 的那条规则大致是:

location ~ [^/]\.php(/|$) {
    fastcgi_pass unix:/tmp/php-cgi-83.sock;
    ...
}

它只认 .php 结尾。xxx.php.bak-20261006 不是 .php 结尾,Nginx 就把它当成静态文件,原样返回给访客。源码、写在代码里的密钥、数据库连接方式,全在里面。如果哪天有人备份了 wp-config.php,数据库账号密码就公开了。

还有一层:站点前面挂着 Cloudflare 的话,这些文件被访问过一次,边缘就可能缓存一份。

三、先查自己有没有

在服务器上数一下(宝塔的网站目录默认是 /www/wwwroot):

find /www/wwwroot -type f \( -iname '*.bak*' -o -iname '*.old' -o -iname '*.orig' \
  -o -iname '*.tsv' -o -iname '*.sql' -o -iname '*.sql.gz' \) | wc -l

不是 0 的话,把列表打出来,挑几个从外网用上面的 curl 试一下。返回 200 就是能被下载。

.sql 和 .sql.gz 也要查:有些备份插件或者手动导出的数据库会放在网站根目录。

四、挪出网站目录,先别删

我没有直接删,而是原样挪到网站目录外面,目录结构不变,万一哪个文件有用还能找回来:

D=/www/backup/web-backups-$(date +%Y%m%d)
cd /www/wwwroot
find . -type f -iname '*.bak*' -print0 | while IFS= read -r -d '' f; do
  mkdir -p "$D/$(dirname "$f")"
  mv "$f" "$D/$f"
done

挪之前先确认网站代码里没有地方引用这些备份:

grep -rn 'bak-20' /www/wwwroot --include='*.php' | head

没有输出就可以挪。挪完再跑一遍第三步的 find,结果应该是 0。

有两个目录归 root 所有,普通用户挪不动,要用 root 单独处理一下。

五、挪走以后,记得清 Cloudflare 缓存

挪完从外网再试一遍。如果还是 200,看一下响应头:

curl -sI https://example.com/robots.txt.bak-20261018-033813 | grep -iE '^HTTP|cf-cache-status'
HTTP/2 200
cf-cache-status: HIT

HIT 说明返回的是 Cloudflare 边缘缓存的旧副本,源站上已经没有了。在 Cloudflare 后台清一次缓存(Caching → Purge Everything,或者按网址清),再试应该就是 404。我这次 4 个站都在 Cloudflare 后台清了一遍,复查都是 404。

六、Nginx 兜底:像备份的文件名一律 404

前面几步靠的是有人记得。以后谁又把备份放错地方,最好也拿不到。新建一个规则文件 /www/server/nginx/conf/gk-deny-backups.conf:

# 备份类文件名一律 404(例:x.php.bak-20261021、x.old、x.tsv、x.sql)
location ~* \.(bak|old|orig|removed|broken)([-._][^/]*)?$ { return 404; }
location ~* \.(tsv|sql|sql\.gz)$ { return 404; }

第一条匹配 .bak 结尾,也匹配 .bak-20261021、.bak.1 这种带后缀的写法。

然后在每个站的配置里(宝塔:/www/server/panel/vhost/nginx/站点名.conf),server_name 那行下面加一行,放在其他 location 前面:

include /www/server/nginx/conf/gk-deny-backups.conf;

正则 location 按出现的先后匹配,放在前面,就不会被别的规则抢先。改完先检查再重载:

nginx -t && nginx -s reload

验证:在网站目录里放两个测试文件,一个普通的 .txt,一个 .txt.bak-日期:

200 gk-rule-test.txt
404 gk-rule-test.txt.bak-20261104

普通文件正常,备份名 404。测完把两个文件删掉。

在宝塔里改过网站设置以后,可以 grep gk-deny-backups 一下配置文件,确认这一行还在。

七、以后备份放哪

  • 放在网站目录外面,比如 /www/backup/。外网访问不到,也不会跟着网站一起被打包下载。
  • 改配置的流程里,凡是写了「先备份」,都写明备份放在哪个目录。这次的问题就出在只写了前半句。
  • 隔一段时间跑一遍第三步的 find,结果应该一直是 0。

记下来的几件事

  1. .php.bak 不会被执行,会被当成文本下载。备份比原文件更危险。
  2. 挪走以后先看 cf-cache-status,是 HIT 就去清 Cloudflare 缓存。
  3. 写「先备份」的时候,顺手写上「放在哪」。

© 2026 本文由 Charlie 原创撰写,发布于 sodebug.com。未经授权禁止转载、洗稿、机器抓取。AI 训练数据使用需获得书面授权。

独立开发者,在 WordPress、Nginx 和各种 API 之间切换。不写没用的东西。

相关文章

Nginx 双重 Cache-Control 覆盖陷阱:add_header 同名冲突与 Cloudflare 不缓存修复6 min宝塔 Nginx 限流配置速查:limit_req_zone 参数详解4 minNginx FastCGI Cache 缓存清除完整指南:为什么改了首页却看不到更新5 minNginx 限流实战:用 limit_req 防 CC 攻击,误伤正常用户怎么办?5 min
$ echo "less bullshit, more debugging" | send-to-inbox
新文章直接发到邮箱,一个月 2-4 封。
订阅