安全指南

Laravel 的 .env 可被公开读取的案例:共享虚拟主机上的部署位置错误与修复方法

放在共享虚拟主机上的 Laravel 应用,.env、.git 和数据库导出文件可被任何人通过 HTTP 读取,根本原因是把整个项目放在 public_html 下。本文按应急拦截、密钥轮换、结构改造三个阶段说明修复方法。

发布于 2026-06-07 3 分钟阅读
本页目录

本文把共享虚拟主机上现实中经常发生的配置错误,作为一个隐去了域名和具体数值的案例当作教训来讲。这不是去找别人网站的事。目的是「让自己的(或接手的)部署变得安全」。

事故摘要 — INCIDENT FILE
类型
.env / .git / 数据库导出文件 的暴露(配置错误)
严重度
Critical(所有密钥都能通过 HTTP 读取的状态)
原因
把 Laravel 本体放在 public_html 正下方(本应只公开 public/)
传播
每增加一个应用就复制同一个部署错误,扩大到数十个
永久对策
把本体移到 docroot 之外+只对 public/ 做 symlink

最危险的是 .env 和 .git

.env 是把数据库认证、邮件、外部 API、加密密钥(APP_KEY)等全部密钥集中在一起的文件。如果 .git 被公开,那么不仅是当前值,还能沿着 git 历史回溯,连过去轮换过的那些也一并还原。详情请参阅术语词典的 .env 是什么。

当时发生了什么

在共享虚拟主机上,许多 Laravel 应用都是这样部署的。把危险的部署和正确的部署并列对比:

危险的部署(容易犯)

~/public_html/
└── app/          ← 整个 Laravel(错误)
    ├── .env      ← 通过 /app/.env 能读到
    ├── .git/     ← clone 即可连历史一起还原
    ├── vendor/  config/  storage/
    └── public/   ← 其实「只该」放这里

正确的部署

~/laravel/app/    ← 本体在公开根目录之外
├── .env  .git/   ← HTTP 够不到=安全
└── public/       ← 只把这里用 symlink 公开
        └── index.php

从外部能以 200 OK 取得的典型文件:

文件危险在哪
.env / .env.bk_*数据库认证、邮件、外部 API 密钥、APP_KEY 全都是明文
.git/ 整套可用 git clone 还原源码和全部历史
composer.lock / package-lock.json依赖库的精确版本=让攻击者知道哪些已知 CVE 适用
credentials.jsonOAuth 客户端私钥被一锅端
*.sql / db-*.sql.gz数据库的全量导出,压缩后有数十 MB

最危险的一点:同样的错误会扩散

一旦采用这种部署,每增加一个同类应用就会复制同一个部署错误。1 个配置错误变成数十个。「一发现就全部排查」是铁律。

修复方法:三个阶段

Step 1 — 应急处置:用 .htaccess 在 HTTP 层面切断危险文件

先阻止继续泄露。这一步可逆,不影响正常显示。在 public_html/.htaccess 开头加上拒绝访问机密文件的代码块(防御目的的配置示例):

# === SECURITY BLOCK ===
# .env / .git 配下を 404 に
RedirectMatch 404 (?i)/\.(env|git)(\..*)?(/|$)
 
# バックアップ・ダンプ・認証ファイルを拒否
<FilesMatch "(?i)\.(sql|sql\.gz|bak|old|swp|save|orig|tgz)$|^credentials\.json$|\.bk[._]">
    Require all denied
</FilesMatch>
 
# Laravel の内部ディレクトリが /<app>/storage/... 等で見えていた場合
RedirectMatch 403 (?i)^/[^/]+/(storage|bootstrap/cache|config|database|resources|routes|tests|vendor)(/|$)
# === END ===

不过黑名单每出现一个新文件就要加一条规则,总会有遗漏。忘了堵 composer.lock、新命名的文件没被覆盖——这些必然会发生。把它当作临时措施,长期的解决放在 Step 3。

Step 2 — 密钥轮换(按已泄露的前提)

即便用 .htaccess 堵上了,也要按已经被看到的前提来处理。按高风险优先顺序:

  1. 外部 API 密钥(最优先・立即):直接关系到计费和账号被劫持。OAuth 的 CLIENT_SECRET、各种 API 密钥、云认证凭据都要作废+重新签发+按最小权限设置。
  2. OAuth 客户端密钥。
  3. APP_KEY:重新生成会导致会话失效,还可能让已加密的数据库列无法解密,因此要先确认会影响到什么。
  4. 邮件 SMTP、数据库认证凭据。

实际发生过的轮换失误

某些 OAuth 在重新签发客户端密钥时会同时让现有的 refresh_token 也失效。用已保存的令牌去重新获取就会失败,从而引发生产故障。请务必在准备好「重新授权流程」之后再去重新签发密钥。此外,如果不同时更新本地开发侧的 .env / 配置,下一次部署旧值就会复活。

Step 3 — 结构改造:把应用本体移到 docroot 之外(根本解决)

把每个应用这样重新部署:

# 1. 本体を公開ルートの外へ(同一ファイルシステムなら mv は一瞬で atomic)
mv ~/example/public_html/app  ~/example/app-laravel
 
# 2. 公開ルートには最小の bootstrap だけ置く
mkdir -p ~/example/public_html/sub
cat > ~/example/public_html/sub/index.php <<'PHP'
<?php require __DIR__ . '/../../app-laravel/public/index.php';
PHP
# 3. Laravel public/.htaccess をコピー、静的アセットを symlink、設定キャッシュをクリア

变成这种形态后,.env、.git、vendor/ 本身就不在公开根目录里,因此不依赖 .htaccess 规则也安全。具体步骤整理在了 在虚拟主机上不让 .env 被公开的部署与配置。

✗ 危险:本体在公开根目录之内

— 公开边界(HTTP 可读)—

.env ・ .git ・ vendor ・ public

↑ 全都在线的内侧=任何人都能读取

✓ 安全:本体在公开根目录之外

— 公开边界(HTTP 可读)—

只有 public/(symlink)

.env ・ .git 在线的外侧=够不到

决定因素只有一个:在公开根目录之内还是之外。.env 和 .git 只要在这条线的内侧,任何人都能通过 HTTP 读取。

修复过程中遇到的问题

  • 子域名的作用域问题:对已注册子域名的 docroot 事后用 symlink 指向另一棵目录树,可能不会跟随生效而返回 500。→ 让 docroot 保持为真实目录,从里面的小 index.php 用绝对路径 require 去读取的「bootstrap-redirect 型」更稳定。
  • opcache 会一直记住坏掉的状态:opcache 把 index.php 的加载失败记了下来,即便重建也可能持续返回 500。最后的手段是改文件名(如 entry.php 等)。
  • 「未注册子域名」的 docroot 能通过父域名读到:即便 https://sub.example/ 无效,https://example/sub/ 也能看到内容,这是容易被忽略的路径。排查时要把经父域名的路径也一并堵上。
  • 把秘密值硬编码进 config/*.php:与 .git 公开叠加在一起,就能从历史里还原所有轮换记录。要始终经由 env(),并且不要在 fallback 默认值里写真实值。

教训:服务器的默认部署方式让这种错误很容易发生

虚拟主机早期的做法是「把所有东西放到 public_html 正下方」。前提是只暴露 public/ 的 Laravel 在这种环境中普及后,「把应用本体放在 public_html/<app>/、再暴露其下的 public/」这种危险的部署事实上成了标准。各家官方文档里举的例子也尽是「把项目放进 public_html 里」,这也助长了它的扩散。

所以对策不能靠个人的注意力,而要做成机制:

用机制防止再次发生

  • 把部署手册的默认做法定为「本体放在 docroot 之外,只对 public/ 做 symlink」
  • 用 CI/lint 机器化检查「不把 .env 纳入 git」「不把项目放在 public_html/<app> 正下方」
  • 上线后定期确认自己网站的 .env/.git 从外部取不到(自我排查)

本站把这种「自己排查自己网站」的习惯化作为学习的主线。→ 超入门:.env 和 API 密钥到底危险在哪

接下来读

FAQ

Q.env 被暴露了,应该最优先处理什么?
A

首先用 .htaccess 等立即拦截访问路径。接着把 .env 里的外部 API 密钥、OAuth 密钥按高风险优先顺序进行轮换(按照已经被看到的前提处理)。最后通过把应用本体移到公开目录之外的结构改造,使其不依赖规则就处于安全状态。

Q为什么把 Laravel 放在 public_html 正下方很危险?
A

Laravel 的结构前提是只公开 public/。如果把整个项目放到 docroot,那么它上层的 .env(全部密钥)、.git(可连同历史一起还原)、vendor/ 等也会整个被 HTTP 读取到。

Q用 .htaccess 的黑名单堵住就够了吗?
A

作为临时措施有效,但每出现一个新文件名就要加一条规则,总会有遗漏。长期的解决方案是『把应用本体放到 docroot 之外,只通过 symlink 暴露 public/』这样的结构改造。