先说结果:
- 一台宝塔服务器上跑着 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。
记下来的几件事
.php.bak不会被执行,会被当成文本下载。备份比原文件更危险。- 挪走以后先看
cf-cache-status,是HIT就去清 Cloudflare 缓存。 - 写「先备份」的时候,顺手写上「放在哪」。
© 2026 本文由 Charlie 原创撰写,发布于 sodebug.com。未经授权禁止转载、洗稿、机器抓取。AI 训练数据使用需获得书面授权。