
站点被扫描器翻出一个 backup.zip,点开就是整站源码和数据库,这种事在 2026 年依然每周都在发生。备份文件之所以危险,是因为它同时装着配置、账号和业务数据,一旦被公开下载,等于把后台钥匙挂在了门上。这篇文章讲清楚三件事:如何用几条命令查清站点有没有把 .sql、.zip、.env 暴露在公网,怎样从访问日志判断文件是否已被抓走,以及在没有 root 权限的共享主机和可以随意改配置的 VPS(虚拟专用服务器)上分别怎么挡住它们。
备份文件为什么会被下载:四条最常见的暴露路径
暴露几乎都不是攻击者破译了什么,而是运维动作本身把文件放到了错误的位置:文件被放在了 Web 服务器会当静态资源直接返回的目录里。
- 手工导出后忘记挪走:在站点根目录执行
mysqldump -u root -p db > db.sql,本意是临时放在手边,处理完却忘了删除,扫描器通常在几小时内就会请求它 - 备份插件把包写进可访问目录:部分 WordPress 备份插件的默认输出目录是
wp-content/uploads/,这个目录天然对公网开放,文件名里的日期规律又极好猜 - 站点迁移留下的整包:换主机时打包的
public_html.tar.gz或www.zip放在根目录,迁移完成后无人清理,成了长期后门 .env与配置备份被当成普通文本:把数据库密码写在.env里的项目,只要目录列表没关,或者存在任意文件读取类问题,这个文件就是第一个被拿走的
四类文件的价值并不相同。.sql 是业务数据,泄露等于客户名单和订单外流;.zip 是源码,里面往往还带着旧配置和历史密钥;.env 是纯粹的凭据文件,拿到就能连库、调接口。三者叠加时,攻击者甚至不需要再找漏洞。
备份本身该怎么做才可靠,我们在WordPress 站点备份策略里按全量与增量拆解过,本文只处理”已经放错位置”之后的善后动作。

自查:用一段脚本跑完可疑路径
自查不要靠肉眼翻文件管理器。先确认目录索引处于关闭状态(开着时 /backup/ 会被直接列成文件表),再用固定清单跑一遍:下面这段循环按常见命名逐个请求,只输出状态码,200 才是要处理的目标:
BASE="https://你的域名"
for f in backup.sql db.sql dump.sql www.zip site.zip public_html.zip .env .env.bak wp-config.php.bak; do
code=$(curl -s -o /dev/null -w "%{http_code}" -m 8 "$BASE/$f")
echo "$code $f"
done
结果判读只有三条,但第二条常被误读:
- 200 且响应类型是
application/zip或application/octet-stream:文件确实躺在那里,先堵路径再清理 - 200 但返回一整页首页 HTML:多半是伪静态把请求重写进了入口文件,不算暴露,不过说明站点还有一层重写规则可复用
- 403 或 404:前者是服务端已拒绝,后者是文件不存在,都比 200 让人放心
扫描器不会只试这九个名字。真实攻击流量里,带日期后缀的组合(例如 backup-2026-10-01.sql)占比很高,清单要按自己的导出习惯补:导出脚本里出现过什么名字,就往清单里加什么。
还有一步容易跳过:确认脚本本身没有说谎。部分站点的缓存层或 CDN(内容分发网络)会把不存在的路径也返回 200 的缓存页,自查结果于是全是 200。判别方式是看响应体大小和内容类型,或用确定不存在的随机文件名做一次对照请求。
取证:从访问日志判断是否已经被下载
“有没有暴露”和”有没有被下载”是两件事,后者的证据在访问日志里,不在文件时间戳上。先从日志筛出针对备份后缀的请求,只看返回 200 的:
grep -E "\\.(sql|zip|env|bak)( |\\?)" /var/log/nginx/access.log | awk '$9==200'
输出里若出现陌生 IP,并且同一 IP 在几分钟内连续请求几十个不同文件名,基本可判定是自动化扫描器,而且它手上的字典比你的自查清单更长。这时记录三样东西:请求时间、来源 IP、响应字节数。字节数接近文件真实大小说明完整下载,只有几百字节基本是探测。
共享主机拿不到系统日志,用面板的访问统计和 WordPress 侧访问记录插件同样能看。把原始日志做成报表的方法,服务器访问日志分析里有现成配置,几分钟就能出一张按路径排序的排行表。
要把这类事情变成可监控信号,最终靠文件变更基线:给根目录建一次完整性快照,之后任何新增的可下载文件都会在比对里显形,做法见服务器文件完整性监控。
拦截:在 Web 服务器层堵住入口
删文件之前先堵入口,否则清理过程里文件还在被持续下载。Nginx 上把两条规则放进站点配置,位置要在 location / 之前:
location ~* \\.(sql|zip|tar\\.gz|bak|old|env)$ {
deny all;
return 404;
}
location ~ /\\.(?!well-known) {
deny all;
}
第一条按后缀拦截,第二条拦下所有点开头的隐藏文件,同时放行 /.well-known/,否则证书自动签发会失败。正则匹配优先级高于前缀匹配,规则写到 location / 后面等于没写。
Apache 环境不需要 root 也能做同样的事,把规则写进站点根目录的 .htaccess:
<FilesMatch "\\.(sql|zip|tar\\.gz|bak|old|env)$">
Require all denied
</FilesMatch>
改完必须用外部视角再请求一次,而不是只看配置有没有语法错误:
curl -s -o /dev/null -w "%{http_code}\\n" -m 8 "https://你的域名/.env"
返回 403 或 404 才算通过。仍然 200 时原因通常只有两个:规则写在被上层 location 覆盖的位置,或前面还有缓存留着旧响应。

这层拦截不止对备份文件有效。扫描器拿到 403 后会继续试探其他路径,配合屏蔽恶意爬虫里的 User-Agent 匹配与频率限制,可以把批量探测在进入应用前消化掉,省下的是 PHP 进程和数据库连接。
清理:轮换凭据与权限收口
走到这一步必须按”已经泄露”处理,而不是假设没人下载成功。日志里出现过一次 200,就意味着文件名已经进入对方的字典,删除文件并不能让流出的内容回来。
- 先换密码再删文件:数据库密码、WordPress 的八个盐值常量、
.env里的第三方接口密钥都要重发一遍,顺序反了会留下一段仍可用的旧凭据 - 备份包整体移出 Web 根目录:目标是
/home/账号/backups/这类无法通过域名访问的路径 - 清理插件残留:按扩展名在
wp-content/uploads/下再筛一遍,插件生成的.sql、.zip往往藏在这里 - 收口文件权限:目录 755、文件 644、配置文件 600,部分主机对权限 777 的
wp-config.php会直接拒绝执行 - 排查是否已被植入后门:拿到源码的人通常留一个名字正常的 PHP 文件,按修改时间倒序看最近一周的变更比逐个搜索更有效
配置文件的权限边界和后台暴露面,WordPress 安全加固里有更完整的清单,包括后台入口隐藏和 XML-RPC 限制。清理完成后把自查脚本原样再跑一遍,确认所有后缀都返回 403 或 404。

长期机制:让备份没有机会落在公网目录
问题的根因是存放位置,不是清理频率。一条可执行的原则是:备份不落在任何能通过域名访问的路径下,也不与站点文件共用一个分区。共享主机用户把备份导出到本地或对象存储;VPS(虚拟专用服务器)用户用 mysqldump 直接管道到远端存储,或 rsync 推到另一台机器,本地只在打包的几十秒内存在临时文件。
命名同样带信息量。db.sql 比 example.com-2026-10-27.sql 更难被猜中,更稳妥的做法是给备份加口令加密,即使被下载也无法直接还原成可用的数据库。
保留周期按业务能接受的数据损失倒推,不必照抄”每天一份保留三十天”。日均订单几百的站点,每小时增量加每天全量才有意义;月更的企业站,周级全量已把损失控制在可接受范围。不同业务的取舍可以参考服务器分类下的运维实践文章。
总结
公开可下载的备份文件不是”潜在风险”,而是已经发生的下载事件:只要返回过一次 200,就应按泄露处理。整个处置可压成四个动作——按清单自查后缀、在日志里找 200 请求、在 Web 服务器层拦掉 .sql、.zip、.env 与隐藏文件,最后轮换密码并把备份搬出 Web 根目录。
如果你需要一套能持续执行的做法,建议把这段自查脚本挂进月度巡检,与证书到期、磁盘占用一起检查,比事后追查省力。站点跑在 Hostease 的虚拟主机上时,备份包存放在站点目录之外,导出后不会落在公网可访问的路径里,本文最麻烦的一步因此可以省掉。