安全指南

是否把密钥文件遗忘在了公开目录里:webroot 盘点

令牌或认证凭据的文件,是否被遗忘在了 Web 公开目录(webroot)里?任何人都能通过 URL 取走的典型失误、共用模板如何把同一个文件复制到所有站点,以及如何盘点自己的服务器并把洞堵上,本站从运维视角讲解。

发布于 2026-06-11 更新于 2026-10-06 6 分钟阅读
本页目录

适用对象:在虚拟主机/共用服务器或 VPS 上运营多个站点、部署到公开目录(public/、public_html/、htdocs/ 等)的人。这里不讲攻击步骤,只讲盘点自己的服务器并把洞堵上。.env 本身的保护方法,见 共用服务器上的 .env 保护。

本站的视角:放在哪里,本身就是访问控制

密钥泄露的原因,比起高明的攻击,更多是放错位置的事故。webroot 是「任何人都能通过 URL 来取的地方」。一旦把密钥放进那里,服务器的密码再强也毫无意义。本站的基本做法是密钥既不放进 webroot 也不放进 git(→ 密钥的安全保管)。思路很简单:公开目录里只放被任何人看到也无所谓的东西。

public/ = 任何人都可通过 URL 取走

可以放

图片、CSS、JS 等公开资源

不要放

.env、token.json、.sql、.git、.bak

应用根目录之上 = 不对外提供

放在这里

密钥、令牌、认证凭据、备份。权限 600(仅所有者可读)。无法通过 URL 访问。

公开目录里的文件任何人都能通过 URL 取走。密钥放到它之外(不对外提供、权限 600)。

为什么会发生「公开目录里的遗忘」

常见的情形是:为图方便而放的文件,或样板里自带的文件,就这样一直处于公开状态。

一个 URL
webroot 里的文件任何人都能取走
数年
在无人察觉中一直放着
所有站点
模板里的遗忘文件会扩散到所有站点
立即吊销
假定已泄露并轮换

例如,一个存有第三方集成访问令牌的 JSON 长期放在 public/ 之下,而如果它源自共用模板,同一个文件会被复制到许多站点的同一位置,它们都有同样的问题。只修好一个站点,其他站点仍留着这个文件,就毫无意义。同样危险的,还有遗留的 .env、数据库备份(.sql)、.git 目录、编辑器备份(*.bak)和快捷方式(相关:路径穿越 · .env 被整个公开的事)。

容易被遗留的文件,以及各自的危险

提到「密钥文件」,人们通常会想到 .env,但实际被遗留的范围要广得多。OWASP(专注于 Web 安全的非营利组织)的测试指南也把旧版本文件、备份以及没有任何链接指向的文件列为检查对象。

类型典型文件名面临的风险应放在哪里
配置文件.env、config.php.bak、settings.json以明文写着的数据库密码和 API key应用根目录之上(不对外提供)
令牌和认证凭据token.json、credentials.json、*.pem、*.key可直接登录外部服务或其他服务器webroot 之外,权限 600
手动或编辑器备份login.php.old、index.php~、.index.php.swp、*.orig扩展名变了,文件可能不会作为代码执行,源代码会以纯文本显示不要在生产环境中创建;在本地编辑
数据库转储backup.sql、dump.sql.gz用户的个人信息和密码哈希,全在一个文件里专用的备份位置,最好在服务器之外
站点压缩包site.zip、www.tar.gz包含 .env 和全部源代码解压后立即删除
版本控制数据.git/过去提交中的密钥可能被还原公开目录之外
构建时嵌入的值assets/index-xxxx.js 之中以 VITE_ 或 NEXT_PUBLIC_ 开头的变量会进入浏览器端的 JavaScript密钥只放在服务端变量中

最后一行很容易被忽略。即使你从未放过文件,构建也可能把密钥推进公开目录。Vite 的官方文档明确指出,VITE_ 变量不得包含 API key 等敏感信息。

盘点自己的服务器

首先,机械地找出「自己的公开目录里有没有看起来像密钥的文件」。这是检查自己的资产,而不是窥探别人的站点。

1

在公开目录之下搜索看似密钥的文件

在自己的 home 目录下,找出混入 public/ 的密钥候选(令牌/认证凭据的 JSON、.env、密钥、快捷方式)。
2

假定其他地方也有同一文件,检查所有主机和站点

源自同一样板的站点,在同一位置有同样的东西;对所有主机和站点执行同样的检查。发现一个,通常意味着还有更多。
3

逐个判断是否真的需要公开

把每个结果分为「可以公开」和「密钥」。密钥按下面的移除、吊销、迁移步骤处理。
# audit under your own public directories (inspecting your own assets)
find ~ -path '*/public/*' \( -iname '*token*.json' -o -iname '*credential*.json' \
      -o -iname '*.bak' -o -iname '*.sql' -o -iname '.env*' \) 2>/dev/null
# an exposed .git directory is dangerous too
find ~ -path '*/public/*/.git' -maxdepth 8 2>/dev/null

如何检查自己的环境

上面的 find 只是起点。按下面五项逐一检查,能发现仅靠文件名搜索会漏掉的遗留文件。只对自己的服务器和自己的域名执行。.env 特有的检查(查看响应的开头字节、确认 .htaccess 确实生效)见 共用服务器上的 .env 保护。

1

也搜索备份和压缩包

公开目录的名称因主机而异,所以要包括 public_html、htdocs 和 www。例如运行 find ~/public_html \( -name '*~' -o -name '*.swp' -o -name '*.old' -o -name '*.orig' -o -name '*.zip' -o -name '*.tar.gz' -o -name '*.pem' -o -name '*.key' \) 2>/dev/null,逐个确认每个结果是否真的应该公开。

2

确认目录列表已关闭

在浏览器中打开一个没有 index.html 的目录(例如 /uploads/)。如果看到文件名列表,说明 Web 服务器的目录列表功能是开着的。有了列表,别人不知道文件名也能发现遗留文件。返回 403 或 404 才是正确的。

3

在构建出的 JavaScript 中搜索密钥

如果你构建前端,就搜索要部署的输出文件夹(dist/、.next/static/ 等)。可以用 grep -rlE 'BEGIN [A-Z ]*PRIVATE KEY|secret|password' dist/ 之类的命令,再加上你所用服务的 key 前缀。如果找到,检查该变量是否带有面向浏览器的名称(VITE_、NEXT_PUBLIC_)。

4

确认 webroot 之外的密钥不能被其他用户读取

把密钥移出 webroot,如果权限宽松、同一服务器上的其他账户能读取,也没有用。find ~ -maxdepth 3 \( -name '.env*' -o -name '*.pem' -o -name '*token*.json' \) -perm /o+r 2>/dev/null 会列出所有者以外的人可读的密钥;用 chmod 600 修正。没有输出就说明没问题。

5

检查搜索引擎是否收录了什么

在 Google 中搜索 site:你的域名,查找不认识的文件(.sql、.zip、.txt 等)。如果你使用 Search Console,「Pages」(网页)报告会列出已编入索引的 URL。如果仍留在搜索结果里该怎么办,见下面的常见错误。

发现时的处理

把移除、吊销、迁移三件事成套执行。缺少任何一件,处理都不完整。

1

从 webroot 移除,使其无法通过 URL 取走

把文件移到公开目录之外或删除,并确认该 URL 现在返回 404。
2

吊销或重新签发可能已泄露的密钥或令牌

不要以「大概没事」为由放着。既然曾经暴露,就假定已泄露,刷新或吊销该令牌或密钥。
3

把密钥放到 webroot 之外,权限 600

今后把密钥放在公开目录之外(例如应用根目录之上),权限设为 600(仅所有者可读)。如果配置中可以拒绝提供敏感扩展名的文件,也一并加上。

按服务器:拒绝提供这些文件的设置(作为兜底)

真正的修复是不把文件放在那里。在此之上,在 Web 服务器上加一条拒绝规则,即使误放了也不会被提供出去。能用什么取决于你的环境。

环境拒绝方法目录列表注意点
Apache(VPS)在 <Directory> 块或 .htaccess 中使用 <FilesMatch> + Require all denied用 Options -Indexes 关闭Apache 2.4 中 AllowOverride 默认为 None,除非允许,否则 .htaccess 会被忽略
Nginx(VPS)在 location 中写 deny all;autoindex 默认为 off不读取 .htaccess。正则 location 块按书写顺序检查,第一个匹配的生效
共用服务器通常用 .htaccess(兼容 Apache)通过控制面板或 .htaccess用一个不含任何密钥的测试文件确认它是否生效
静态托管 / CDN输出文件夹里的内容会原样发布取决于服务依靠让密钥不进入输出文件夹,而不是依靠拒绝规则

Apache 示例(.htaccess 已启用的情况):

Options -Indexes
<FilesMatch "(^\.|~$|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key)$)">
    Require all denied
</FilesMatch>

Nginx 示例(在 server 块内):

# Allow the path used for automatic certificate renewal (Let's Encrypt) first
location ^~ /.well-known/acme-challenge/ { }
 
# Deny files and directories starting with a dot (.env, .git, ...)
location ~ /\. { deny all; }
 
# Deny backups and archives
location ~* (~|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key))$ { deny all; }

在 Nginx 中,当请求匹配到标有 ^~ 的 location 时,不会再检查正则 location。这样,尽管有拒绝以点开头路径的规则,用于证书续期的 /.well-known/acme-challenge/ 仍然可以访问。修改配置后,用 nginx -t 检查语法,重新加载,并确认测试文件返回 403。如果你的站点提供 ZIP 等文件下载,就把该扩展名从拒绝列表中去掉。

容易落入的放置方式(危险)

  • 集成用的令牌 JSON 放在 public/ 中
  • 样板里自带的密钥文件一直公开着
  • 备份(.sql/.bak)或 .git 留在公开目录之下
  • 只修一个站点就当作「处理完毕」

正确的放置方式

  • 公开目录里只放可以公开的东西
  • 密钥放在 webroot 之外,权限 600
  • 敏感扩展名拒绝提供
  • 发现一处就排查所有主机和站点

把「只放可以公开的东西」定为常规规则

规则胜过一次性的修补。定下公开目录只放可以公开的文件,原则上绝不在那里放密钥、备份、版本控制数据(.git)或配置文件。更新样板时,要从模板本身去掉密钥,否则之后批量生产的每个站点都会继续复制同一个密钥文件。

常见错误及修正方法

  • 依赖拒绝规则,把文件留在原处:迁移服务器或重写配置时规则可能丢失,从那天起文件就公开了。Nginx 根本不读取 .htaccess。把规则当作兜底,把文件移出公开目录。
  • 直接在生产服务器上编辑文件:编辑器会留下以 ~ 或 .swp 结尾的备份。OWASP 测试指南也建议不要在 Web 服务器上直接编辑文件。在本地编辑,通过部署发布变更。
  • 把整个项目文件夹复制到公开目录:.env、.git 和测试数据会一起出去。只提供构建输出文件夹,并明确指定复制哪些内容。
  • 拒绝所有以点开头的路径,导致证书续期失败:Let's Encrypt 会把验证文件放在 /.well-known/acme-challenge/ 下。像上面的 Nginx 示例那样,先允许这个路径。
  • 把密钥放进 VITE_ 或 NEXT_PUBLIC_ 变量:它们会被嵌入构建出的 JavaScript,任何人都能读到。只在服务端代码中使用密钥,并吊销已经以这种方式发布出去的 key。
  • 删掉文件就到此为止:如果搜索引擎已收录,它可能还留在结果里。确认 URL 返回 404 或 410,然后在等待重新抓取清除它的期间,用 Google Search Console 的「Removals」(移除)工具暂时隐藏(约六个月)。

真实案例:公开报告中隐藏的个人数据

负责华盛顿特区 Medicaid(面向低收入人群的公共医疗保险)的卫生保健财务部(DHCF)表示,从 2023 年到 2026 年 7 月发布在其网站上的两份统计报告,以「隐藏字段」的形式包含了底层的个人数据。这些报告本来只显示汇总数字,但文件中仍保留着受益人的 Medicaid 编号、出生日期等。其向美国卫生与公众服务部(HHS)提交的报告中,受影响人数为 399,086 人。DHCF 于 2026 年 7 月 21 日获知此事,并撤下了这些报告。

这不是遗忘密钥文件的案例,但根源相同:

  • 公开目录中的文件可以被完整取走,而不只是画面上显示的部分
  • 发布者以为「看不见」的信息,在审核中最容易被遗漏
  • 可能放了好几年才有人察觉

在发布前加一个检查「这个文件里实际有什么」的步骤,既能防止遗留文件,也能防止隐藏数据。完整经过见 华盛顿特区 Medicaid 数据暴露事件(英文)。

本站是怎么做的

本站的基本做法是把密钥(key、令牌、连接字符串)既不放进公开目录,也不放进代码仓库。部署产物只包含构建好的公开资源;密钥作为运行环境的环境变量,保存在不对外提供的地方。原因是:本文这一类事故,正是「一个遗留文件就让任何请求其 URL 的人拿走全部」的典型。我们把放置位置本身设计成访问控制,并以与 漏洞应对的盘点 相同的周期执行这项检查。

接下来阅读

参考资料

  • IPA,"20 Checkpoints for Secure Website Operation and Management"(日文;不应公开的文件、搜索引擎缓存):ipa.go.jp
  • OWASP, "Web Security Testing Guide: Review Old Backup and Unreferenced Files for Sensitive Information"(.old、~、.swp 备份;不要在服务器上直接编辑):wstg.owasp.org
  • Apache HTTP Server 2.4, "Upgrading to 2.4 from 2.2"(AllowOverride 默认为 None)和 "mod_autoindex"(Options +Indexes 会生成列表):httpd.apache.org, httpd.apache.org
  • nginx, "ngx_http_core_module: location"(匹配顺序与 ^~)和 "ngx_http_autoindex_module"(autoindex 默认为 off):nginx.org, nginx.org
  • Let's Encrypt, "Challenge Types"(HTTP-01 使用 /.well-known/acme-challenge/):letsencrypt.org
  • Vite, "Env Variables and Modes"(VITE_ 变量会暴露给客户端代码):vite.dev
  • Next.js, "Environment Variables"(NEXT_PUBLIC_ 变量会内联到浏览器端 JavaScript 中):nextjs.org
  • Google Search Console 帮助, "Removals tool"(临时移除约持续六个月):support.google.com
  • District of Columbia DHCF, "Notice of Data Incident":dhcf.dc.gov;HIPAA Journal(向 HHS 报告的人数):hipaajournal.com

FAQ

Q把文件放在公开目录里有什么危险?
A

放进 Web 公开目录(webroot,即 public/、public_html/ 等)的文件,任何访问其 URL 的人都能取走。令牌或认证凭据的 JSON、.env、备份、密钥文件一旦遗忘在那里,会在无人察觉时被取走,立刻造成实害。公开目录只应放可以公开的东西。

Q如果在一个站点发现了,是否应该排查其他站点?
A

应该。用共用模板或样板批量生产站点时,同样的遗忘文件通常已被复制到所有站点。发现一处,就排查源自同一来源的所有站点和主机。

Q发现之后该如何处理?
A

1)把文件从 webroot 移除,使其通过 URL 访问时返回 404。2)刷新或吊销可能已泄露的令牌或密钥(假定已泄露)。3)今后把密钥放在 webroot 之外,权限设为 600(仅所有者可读)。三件事成套执行。

Q哪些文件绝不应该放在 Web 公开目录里?
A

常见的有 .env 等配置文件、令牌或认证凭据的 JSON 文件、数据库转储(.sql)、整个站点的 ZIP 或 tar.gz 压缩包、.git 目录,以及手动或编辑器生成的备份(.bak、.old、.orig、.swp,或以 ~ 结尾的文件名)。OWASP 测试指南说明,如果把 login.php 保留为 login.php.old,服务器可能不会把它当作代码执行,而是把源代码以纯文本形式返回。

Q文件删了,但还出现在搜索结果里,怎么办?
A

先确认该 URL 返回 404 或 410,然后用 Google Search Console 的「Removals」(移除)工具暂时将其从结果中隐藏。根据 Google 的帮助页面,临时移除约持续六个月,应与删除文件等永久性修复配合使用。日本 IPA 同样建议请求搜索引擎删除缓存副本。无论是否已从搜索结果中消失,都要吊销文件中包含的密钥或令牌。

Q可以把密钥放进前端环境变量(VITE_ 或 NEXT_PUBLIC_)吗?
A

不可以。Vite 会把以 VITE_ 开头的变量、Next.js 会把以 NEXT_PUBLIC_ 开头的变量,在构建时嵌入发送给浏览器的 JavaScript 中。这与把文件放进公开目录是一样的:任何人都能读到。API key 等密钥只放在仅由服务端代码使用的变量中。