本文把共享虚拟主机上现实中经常发生的配置错误,作为一个隐去了域名和具体数值的案例当作教训来讲。这不是去找别人网站的事。目的是「让自己的(或接手的)部署变得安全」。
- 类型
- .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.json | OAuth 客户端私钥被一锅端 |
*.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 堵上了,也要按已经被看到的前提来处理。按高风险优先顺序:
- 外部 API 密钥(最优先・立即):直接关系到计费和账号被劫持。OAuth 的
CLIENT_SECRET、各种 API 密钥、云认证凭据都要作废+重新签发+按最小权限设置。 - OAuth 客户端密钥。
APP_KEY:重新生成会导致会话失效,还可能让已加密的数据库列无法解密,因此要先确认影响范围。- 邮件 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 在线的外侧=够不到
路上那些“教科书里没写”的坑
- 子域名的作用域问题:对已注册子域名的 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 密钥到底危险在哪
接下来读
- 术语:.env(环境变量文件)是什么 / CVE 是什么
- 对策:在虚拟主机上不让 .env 被公开的部署与配置
- 事故:用 AI 写的代码泄露了 API 密钥(泄露的“另一条途径”=运行时)
FAQ
Q.env 被暴露了,应该最优先处理什么?
首先用 .htaccess 等立即切断泄露途径(先止血)。接着把 .env 里的外部 API 密钥、OAuth 密钥按高风险优先顺序进行轮换(按照已经被看到的前提处理)。最后通过把应用本体移到公开目录之外的结构改造,使其不依赖规则就处于安全状态。
Q为什么把 Laravel 放在 public_html 正下方很危险?
Laravel 的结构前提是只公开 public/。如果把整个项目放到 docroot,那么它上层的 .env(全部密钥)、.git(可连同历史一起还原)、vendor/ 等也会整个被 HTTP 读取到。
Q用 .htaccess 的黑名单堵住就够了吗?
作为应急处置有效,但本质上是打地鼠。每加一个新命名的文件就多一个漏洞。真正的解决方案是『把应用本体放到 docroot 之外,只通过 symlink 暴露 public/』这样的结构改造。