对象:在共享虚拟主机(使用 Apache/.htaccess 的环境)上放置 Laravel 等「以只公开 public/ 为前提」的应用的人。这是从真实事故(→ .env 全公开的故事)中提炼出来的、用于保护自己环境的步骤。
本站的看法:这不是“个人的疏忽”
虚拟主机早期的习惯是把所有东西都直接放在 public_html 下。以只暴露 public/ 为前提的 Laravel 进入了这种环境,结果危险的目录布局成了事实上的标准,连各家服务商自己的文档也把项目放在 public_html 之内。所以对策要靠机制而不是注意力:改变部署流程的默认做法。
用图掌握正确的目录布局
✗ 危险(主体放在公开根目录之下)
public_html/ ├─ .env ← 能被读到! ├─ .git/ vendor/ └─ public/
✓ 安全(主体在外,只公开 public)
app-laravel/ ← 公开根目录之外 ├─ .env .git/ ← 无法到达 └─ public/ → 从 public_html 处 require
按环境选择对策
「不让 .env 被公开」的具体做法,取决于服务器类型和你的权限。先找到符合自己情况的一行,再按下面的步骤操作。
| 环境 | 长期对策 | 应急处理 | 注意点 |
|---|---|---|---|
| 共享虚拟主机(可用 SSH) | 把应用主体移出公开目录,公开目录里只留一个小的 index.php(Step 2) | 在 .htaccess 里拒绝访问 | 把公开目录本身做成 symlink,在某些环境下会返回 500 |
| 共享虚拟主机(只有 FTP) | 用 FTP 客户端把应用主体重新上传到公开目录(public_html 等)的上一级 | 在 .htaccess 里拒绝访问 | 大多数虚拟主机允许写入公开目录的上一级;不允许时请联系客服 |
| 控制面板可以设置公开目录 | 把公开目录直接指向 app-laravel/public | 不需要(但仍要检查) | 切换后,删除旧公开目录里残留的应用副本 |
| VPS(Apache) | 把 VirtualHost 的 DocumentRoot 指向 public/ | 在服务器配置里写拒绝规则 | AllowOverride None(Apache 2.4 的默认值)时不会读取 .htaccess |
| VPS(Nginx) | 把 root 指向 public/,并拒绝访问以点开头的文件 | 在服务器配置里写拒绝规则 | Nginx 不读取 .htaccess |
无论哪种环境,目标都一样:公开目录里没有 .env、.git、vendor/、storage/。Laravel 官方文档也提醒,如果把 index.php 移到项目根目录并从那里提供服务,会把许多敏感配置文件暴露到公网上。
Step 1:应急处理(立刻能做、可逆)
在公开根目录的 .htaccess 开头追加一段拒绝访问的规则(用于防御目的的配置示例)。它不影响正常页面。
# === SECURITY BLOCK ===
RedirectMatch 404 (?i)/\.(env|git)(\..*)?(/|$)
<FilesMatch "(?i)\.(sql|sql\.gz|bak|old|swp|save|orig|tgz)$|^credentials\.json$|\.bk[._]">
Require all denied
</FilesMatch>
<FilesMatch "(?i)^(phpinfo|info)\.php$">
Require all denied
</FilesMatch>
# if Laravel internals are visible at /<app>/storage/... etc.
RedirectMatch 403 (?i)^/[^/]+/(storage|bootstrap/cache|config|database|resources|routes|tests|vendor)(/|$)
# === END ===应急处理不是终点
黑名单总会漏掉一些东西,比如 composer.lock,或者任何新出现的文件名。只把它当作在 Step 2 完成之前争取时间的手段。
Step 2:结构改造(长期对策)
把应用主体移到公开根目录之外,公开根目录里只留一个只负责加载主体的小入口。
# 1. Move the body outside the web root (same filesystem → mv is instant, atomic)
mv ~/example/public_html/app ~/example/app-laravel
# 2. Put only a minimal entry in the web root
mkdir -p ~/example/public_html/sub
cat > ~/example/public_html/sub/index.php <<'PHP'
<?php require __DIR__ . '/../../app-laravel/public/index.php';
PHP
# 3. Copy Laravel public/.htaccess, symlink static assets
# 4. Clear config cache
rm -f ~/example/app-laravel/bootstrap/cache/{config,services,packages,routes-v7}.php本站运维中的经验:bootstrap-redirect 比 symlink 更稳
把 public_html 本身做成指向 public/ 的 symlink,在某些环境里对已注册子域名的 docroot 不会跟随,会返回 500。让 docroot 保持为真实目录、由其中一个小小的 index.php 用绝对路径 require(即「bootstrap-redirect 型」),在权限受限的共享环境里更稳定。此外 opcache 可能会记住一次失败的加载并持续返回 500,最后的手段是更改入口文件名(如 entry.php)。
Step 3:更换密钥(按已被看到处理)
只要存在过被公开的时段,就把 .env 里的密钥按已被人看到来处理并替换掉。顺序是:外部 API、OAuth 密钥 → 加密密钥 → 邮件 → 数据库。也请参阅 .env 词条。重新签发 OAuth 密钥可能会使 refresh_token 失效,所以要先准备好重新授权的流程。
Step 4:自我检查(养成习惯)
最后,确认自己的站点上确实没有东西被公开(只针对自己拥有的域名)。
# A 200 with a body means it's exposed. 403/404 is okay for now.
curl -sI https://your-domain/.env | head -1
curl -sI https://your-domain/.git/config | head -1每次部署都检查一遍,就能尽早发现目录布局上的失误。
更细致地检查自己的环境
除了 Step 4 的两行命令,下面这些检查能发现更多容易漏掉的问题。只对自己的服务器和自己的域名执行。
不只看状态码,还要看内容
有些应用对任何未知 URL 都返回首页和 200,所以只看状态码判断不了。用 curl -s https://your-domain/.env | head -c 120 之类的命令只输出开头部分,看看有没有 APP_KEY= 或 DB_PASSWORD= 这样的配置行。对于 .git/HEAD,如果出现以 ref: refs/heads/ 开头的行,就说明已被公开。
检查所有应用,而不只是一个
不只检查主域名,子域名和子目录下的每个应用都要重复检查。添加新应用时布局错误往往会被照搬,修好一个不代表其他的也修好了。
在服务器上搜索公开目录
能用 SSH 的话,直接查找危险文件,例如 find ~/public_html -maxdepth 4 \( -name ".env*" -o -name ".git" -o -name "*.sql" -o -name "*.zip" \)。没有任何输出才是想要的结果。只有 FTP 时,在 FTP 客户端里打开「显示隐藏文件」再查看文件列表。
确认 .htaccess 真的生效
在公开目录里放一个不含任何机密的无害测试文件(例如只写了 test 的 check.bak),然后通过 URL 打开。应该返回 403。如果能打开,说明 .htaccess 没有被读取,请确认服务器类型(例如是否为 Nginx)或相关设置。检查完后删除测试文件。
确认调试模式已关闭
在 .env 里,APP_ENV 应为 production,APP_DEBUG 应为 false。Laravel 文档指出,在生产环境中设置 APP_DEBUG=true 有可能把敏感的配置值暴露给用户。能用 SSH 的话,php artisan about --only=environment 也能看到同样的信息。
常见错误及修正方法
- 删了 .env 却留下 .git:被公开的
.git可能让人还原出过去的提交,其中包括旧的.env或密钥。把.git也移出公开目录,并重新签发所有曾经被提交过的机密。 - 在公开目录里留下
.env.bak、.env.old之类的副本:含有真实值的副本和.env一样敏感。把它移出去或删掉。.env.example里只保留占位值。 - 把整个项目打成 ZIP 上传,解压后没有删除:ZIP 里包含
.env。解压后立刻删除。 - 生产环境以
APP_DEBUG=true运行:错误页面可能显示配置值。生产环境一定要设为false。 - 以为写了
.htaccess就安全了:Nginx 和AllowOverride None的环境根本不会读取它。用上面的测试文件方法确认。 - 突然更换
APP_KEY:根据 Laravel 文档,更换密钥会让所有已登录用户退出登录,用旧密钥加密的数据也将无法解密。Laravel 11 及以后版本,在切换前把旧密钥放进APP_PREVIOUS_KEYS。
真实案例:被公开的 .env 被大规模收集
2024 年 8 月,一家安全公司的研究团队报告了一起攻击活动:自动收集 Web 服务器上被公开的 .env 文件,利用其中的云服务凭据把数据复制出去,然后索要赎金。据报告,约 11 万个域名成为目标,被收集的环境变量超过 9 万个,其中约 7,000 个是云服务凭据,约 1,500 个是社交媒体账号凭据。
报告认为原因不在于云服务一侧的漏洞,而在于受害组织的配置错误:他们误把 .env 文件公开了。由此可以得出两点:
- 被公开的
.env不是碰巧被发现的,而是被程序大规模搜索出来的 - 一个
.env就可能把损害扩散到云存储、邮件等其他服务
不要以为小网站就不会被发现。同类布局错误的匿名案例,请看 .env 全公开的故事。
接着阅读
- 事故:.env 全公开的故事(完整记录)
- 术语:什么是 .env
- 入门:.env 和 API 密钥到底危险在哪
- 服务商被入侵时:虚拟主机服务商被入侵时该做什么(自己的设置无法防止的情况)
- 检查入侵:如何检查网站或服务器是否被入侵(如果发现 .env 曾被公开,该从哪里查起)
参考资料
- Laravel 文档「Deployment」(请求应指向
public/index.php,不要从项目根目录提供服务,生产环境APP_DEBUG=false):laravel.com - Laravel 文档「Configuration」(不要把
.env提交到源代码管理;Debug Mode):laravel.com - Laravel 文档「Encryption」(更换
APP_KEY与APP_PREVIOUS_KEYS):laravel.com - Apache HTTP Server 2.4「core - AllowOverride」(设为
None时不读取.htaccess;默认值为None):httpd.apache.org - Apache HTTP Server 2.4「.htaccess files」(何时使用及注意事项):httpd.apache.org
- OWASP「WSTG-CONF-04 Review Old Backup and Unreferenced Files for Sensitive Information」:owasp.org
- Unit 42(Palo Alto Networks)「Leaked Environment Variables Allow Large-Scale Extortion Operation in Cloud Environments」(2024 年 8 月 15 日):unit42.paloaltonetworks.com