暴露的 .env 檔案與洩漏金鑰

暴露的 .env 檔案是一份設定檔——裝著你的資料庫密碼、郵件憑據和 API 金鑰的那一份——你的 Web 伺服器會把名字報出來的人都發一份;暴露的 .git 資料夾是同一種事故,只是還附帶了你全部的原始碼歷史。兩者都不需要攻擊者攻破任何東西:檔案只是放在伺服器會發布的位置上,一次普通的 GET 請求就能把它下載走。本文解釋這種情況如何發生——文件根目錄失誤、把倉庫本身當作站點服務的靜態託管、沒人稽核過的建構產物——發現者會在第一小時拿金鑰做什麼,以及為什麼事後刪除檔案只是整個回應中最小的一部分。想現在就檢查自己的網站,運行洩漏金鑰掃描器,對照結果閱讀。

私有檔案如何變成公開:文件根目錄

每台 Web 伺服器都有一個文件根目錄:其內容按定義即公開的目錄。放進其中的任何東西——有意或無意——都可按 URL 請求,無論是否有頁面連結到它。經典事故是把整個工作目錄複製到伺服器完成部署:本該在那裡的應用程式碼,與它旁邊的 .env 檔案、它裡面的 .git 資料夾、編輯器備份副本、某人在遷移前做的資料庫轉儲、上個月版本的壓縮包一同抵達。伺服器分不清這些。它只負責發檔案;而私有的那些也是檔案。

框架慣例讓這個陷阱很容易踩。.env 檔案本應位於文件根目錄上一層,或完全在部署樹之外,由應用從磁碟讀取——永遠不經過 Web 伺服器。但開發伺服器樂於服務專案根目錄,小型部署把一切拖進 public_html,從教學抄來的設定可能把文件根目錄指向專案本身。發生這種事時表面上一切都不壞,而這正是問題所在:站點運行完美,檔案卻是公開的,第一個注意到的是請求 /.env 的人——自動化掃描器對網際網路上的每個站點持續發出的正是這個請求。

靜態託管、被服務的倉庫與建構產物

靜態託管把伺服器設定從等式中去掉,換成一條更簡單的規則:發布目錄裡有什麼就發布什麼。用輸出目錄設為專案根目錄的方式把倉庫部署到靜態主機,主機便會忠實地服務這個倉庫——如果 .git 被複製進了輸出,它也在其中。暴露的 .git 比看上去更糟:HEAD 檔案和設定證明了布局,物件檔案包含每個曾提交的 blob,用普通 Git 工具就能重建整個歷史——包括那些曾被提交一次、又在後來的提交裡「刪除」的金鑰;那種刪除只把它們從工作樹裡移除,其他任何地方都沒有。

建構產物造成第二類更安靜的暴露。JavaScript 打包器會把程式碼引用的東西內聯,所以在建構時從環境變數讀取的 API 金鑰可能以字面字串結束在交付的包裡——按設計公開、每個訪客可讀——而且不會被任何檔案權限檢查標出,因為這個檔案本就該公開。源映射(source map)又把原始原始碼放在包旁邊,使情況更糟。這一切都不需要設定錯誤的伺服器;只需要一次從未從外部檢查過的建構。

第三種模式是殘留物:部署腳本把壓縮包解到 Web 根目錄並把壓縮包留在那裡,備份外掛把轉儲寫進可經 Web 存取的資料夾,編輯器存下的 config.php.bak——許多伺服器恰恰因為副檔名不再符合 PHP 處理器而把它當純文字發出。每件殘留物都是一件私有之物的完整副本,放在一個可預測的位址上。

攻擊者在第一小時做什麼

發現是自動且持續的。掃描器在大範圍位址上列舉常見路徑——env 檔案、Git 內部檔案、設定名、備份副檔名——新的暴露通常在數小時內被找到,往往更快。抵達你伺服器的是一個不起眼的、返回 200 的 GET 請求;沒有警報,因為從伺服器的角度看什麼異常都沒發生。因此應當假定:任何公開過一天的檔案都已被下載,舉證責任的方向是反過來的。

分診也是自動的。下載到的 .env 在幾秒內被解析:資料庫憑據會被拿去試任何可達的資料庫連接埠,雲端金鑰用一次無害的 API 呼叫驗證是否有效,郵件憑據被試著用來寄信,付款金鑰被檢查其模式——生產環境的付款金鑰和生產資料庫憑據是頭獎,因為每一個都能直接換成錢或資料。有效的金鑰會被迅速使用、交易或持有,因為它們的主人隨時可能輪換它們;暴露的金鑰在第一小時最值錢,而這一個小時被高效地花掉。

為什麼刪除檔案不夠

刪除檔案只能停止新的下載。它對已經存在的副本毫無作用:顯然有發現者的那一份,也有你自己製造的副本——被你的 CDN 或公司代理快取的回應、搜尋引擎或封存服務抓取裡的快照、瀏覽器已經下載的包。在每個快取被清除並過期之前,這個「已刪除」的檔案可能仍被你控制卻忘了清理的基礎設施發出,而你控制之外的那個版本則永遠在你控制之外。

這就是為什麼對已確認暴露的第一反應是輪換而不是刪除:撤銷已暴露的憑據並簽發新的,使全世界的每一份副本在同一時刻變得毫無價值。輪換有順序——簽發替代品、部署它、驗證服務正常、然後再撤銷舊憑據——並且它延伸到已暴露材料能解鎖的一切:一次 .git 暴露意味著輪換曾經出現在歷史中的每一個金鑰,而不只是目前的那些,因為被披露的正是歷史本身。此後,閱讀你對已暴露路徑的存取記錄:它們會告訴你你是否是第一個知道的人,更早的抓取會把一次設定事件變成一份清單完全不同的入侵調查。

探測清單詳解——以及預防清單

像洩漏金鑰掃描器這樣的掃描器只能按名索取檔案,所以它的清單就是那組常見項:/.env 及其環境變體、/.git/HEAD 與 /.git/config、熟悉的設定檔名、備份與壓縮副檔名、金鑰檔名——外加頁面自己的腳本,按金鑰形態的字串搜尋。誠實的局限雙向起作用:掃描器只找它所問的,這就是為什麼它按內容而不是狀態碼確認發現。一個返回你首頁的 200 是全捕獲路由,不是洩漏,任何把它計作洩漏的報告都是在自我膨脹。相關的供應鏈風險——你刻意從他處載入的腳本——見第三方腳本與供應鏈風險。

預防主要是擺放位置加一層拒絕。把金鑰放在文件根目錄之外、倉庫發布輸出之外;讓伺服器即使在危險名字落到那裡時也拒絕它們——針對點檔案、env 與設定副檔名、壓縮包和金鑰材料的拒絕規則,並透過實際請求來測試;從部署產物中剝離 .git 或乾脆封鎖它。在流水線中,在 CI 裡加金鑰掃描——在提交時,讓金鑰在交付前就被攔住;也在建構產物上,讓被打包的金鑰在輸出中被抓到,而不是被陌生人抓到。最後,按計劃並在每次部署變更後親自掃描:本文中的暴露全都是無聲的,而想在別人之前了解自己的暴露,唯一的辦法就是向自己的伺服器問與掃描器相同的問題。

來源與延伸閱讀

常見問題

我刪掉了暴露的 .env 檔案。現在安全了嗎?

還沒有。刪除只能停止新的下載,但副本可能存在於發現者手裡、CDN 與代理快取裡、以及抓取與封存中。請輪換該檔案包含的每一份憑據——撤銷舊金鑰並簽發新金鑰——清除你的快取,並檢查該路徑的存取記錄。只有輪換能讓副本變得毫無價值。

我的站點對 /.env 返回 200。檔案暴露了嗎?

不一定。許多站點對每個未知路徑都以 200 和它們的首頁或應用外殼作答——那是全捕獲回退。重要的是內容:如果回應正文是你的頁面,就沒有暴露;如果是帶真實值的鍵值行,那就是。值得信賴的掃描器按內容確認,並把裸 200 單獨列為未確認。

CI 裡應該跑什麼來阻止這種事再次發生?

在兩個時刻做兩項檢查:對提交做金鑰掃描,阻止金鑰進入倉庫;並檢查建構產物和已部署的輸出目錄,抓住建構內聯進去的和部署複製進去的東西。再加上伺服器對點檔案和金鑰材料的拒絕規則作為最後一層,並在部署變更後重新運行外部探測。