本文把共享虛擬主機上現實中經常發生的設定錯誤,作為一個隱去了網域和具體數值的案例當作教訓來講。這不是去找別人網站的事。目的是「讓自己的(或接手的)部署變得安全」。
- 類型
- .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/』這樣的結構改造。