適用對象:在虛擬主機/共用伺服器或 VPS 上經營多個站點,並把檔案部署到公開目錄(public/、public_html/、htdocs/ 等)的人。這裡不講攻擊手法,只講 盤點自己的伺服器並把漏洞補上。.env 本身的保護方法,請見 共用伺服器上的 .env 保護。
本站的視角:放在哪裡,本身就是存取控制
機密外洩的原因,放錯位置的事故比高明的攻擊更常見。webroot 是「任何人都能透過 URL 來取的地方」。把機密放在那裡,伺服器密碼再強也沒有意義。本站的基本做法是機密既不放進 webroot,也不放進 git(→ 機密的安全保管)。想法很簡單:公開目錄只放任何人看到都無所謂的東西。
public/ = 任何人都能透過 URL 取走
可以放
圖片、CSS、JS 等公開資源
絕不要放
.env、token.json、.sql、.git、.bak
應用程式根目錄之上 = 不提供存取
放在這裡
機密、權杖、憑證、備份。權限 600(僅擁有者可讀)。無法透過 URL 存取。
為什麼會發生「公開目錄裡的遺留檔案」
常見的模式是:為了方便而放的檔案,或樣板裡本來就帶著的檔案,就這樣一直保持公開。
例如,存放第三方整合用存取權杖的 JSON,長期放在 public/ 底下;如果它 來自共用範本,同一個檔案會被複製到許多站點的同一位置,所有站點都有同樣的問題。只修好一個站點,其他站點還留著檔案,就沒有意義。同樣危險的還有遺留的 .env、資料庫備份(.sql)、.git 目錄、編輯器備份(*.bak)和捷徑(相關:路徑穿越 · .env 被整個公開的案例)。
常被遺留的檔案,以及各自的危險
說到「機密檔案」,通常會想到 .env,但實際被遺留的東西範圍廣得多。OWASP(專注於 Web 安全的非營利組織)的測試指南也把舊版本檔案、備份,以及沒有任何連結指向的檔案列為檢查對象。
| 類型 | 典型檔名 | 有什麼風險 | 應該放在哪裡 |
|---|---|---|---|
| 設定檔 | .env、config.php.bak、settings.json | 以明文寫著的資料庫密碼與 API key | 應用程式根目錄之上(不提供存取) |
| 權杖與憑證 | token.json、credentials.json、*.pem、*.key | 可直接登入外部服務或其他伺服器 | webroot 之外,權限 600 |
| 手動或編輯器的備份 | login.php.old、index.php~、.index.php.swp、*.orig | 副檔名改變後可能不會被當成程式碼執行,原始碼會以純文字顯示 | 不要在正式環境產生;在本機編輯 |
| 資料庫傾印 | backup.sql、dump.sql.gz | 使用者的個人資料與密碼雜湊全在一個檔案裡 | 專用的備份位置,最好在伺服器之外 |
| 站點壓縮檔 | site.zip、www.tar.gz | 包含 .env 和完整原始碼 | 解壓後立刻刪除 |
| 版本控制資料 | .git/ | 可能還原出過去提交中的機密 | 公開目錄之外 |
| 建置時嵌入的值 | assets/index-xxxx.js 之中 | 以 VITE_ 或 NEXT_PUBLIC_ 開頭的變數會進入瀏覽器的 JavaScript | 機密只放在伺服器端變數 |
最後一列很容易被忽略。即使你從未放過檔案,建置也可能把機密推進公開目錄。Vite 官方文件明確說明,VITE_ 變數不得包含 API key 等敏感資訊。
盤點自己的伺服器
首先,機械式地找出「自己的公開目錄裡有沒有看起來像機密的檔案」。這是 檢查自己的資產,不是窺探別人的站點。
在公開目錄底下搜尋看似機密的檔案
public/ 的機密候選(權杖/憑證 JSON、.env、金鑰、捷徑)。假設其他地方也有同樣的檔案,檢查所有主機與站點
逐一判斷是否真的需要公開
# audit under your own public directories (inspecting your own assets)
find ~ -path '*/public/*' \( -iname '*token*.json' -o -iname '*credential*.json' \
-o -iname '*.bak' -o -iname '*.sql' -o -iname '.env*' \) 2>/dev/null
# an exposed .git directory is dangerous too
find ~ -path '*/public/*/.git' -maxdepth 8 2>/dev/null如何檢查自己的環境
上面的 find 只是起點。依序完成以下五項檢查,可以找出光靠檔名搜尋會漏掉的遺留檔案。只對自己的伺服器和自己的網域執行。.env 專屬的檢查(查看回應的前幾個位元組、確認 .htaccess 確實生效)請見 共用伺服器上的 .env 保護。
也搜尋備份與壓縮檔
公開目錄的名稱因主機而異,所以要包含 public_html、htdocs 和 www。例如執行 find ~/public_html \( -name '*~' -o -name '*.swp' -o -name '*.old' -o -name '*.orig' -o -name '*.zip' -o -name '*.tar.gz' -o -name '*.pem' -o -name '*.key' \) 2>/dev/null,逐一確認每個結果是否真的應該公開。
確認目錄清單已關閉
用瀏覽器開啟一個沒有 index.html 的目錄(例如 /uploads/)。如果看到檔名清單,代表 Web 伺服器的目錄清單功能是開著的。有了清單,別人不需要知道檔名就能找到遺留檔案。回傳 403 或 404 才是你要的結果。
在建置後的 JavaScript 中搜尋機密
如果你有建置前端,請搜尋要部署的輸出資料夾(dist/、.next/static/ 等)。可以用 grep -rlE 'BEGIN [A-Z ]*PRIVATE KEY|secret|password' dist/ 之類的指令,再加上你所用服務的 key 前綴。如果找到了,檢查該變數是否使用了面向瀏覽器的名稱(VITE_、NEXT_PUBLIC_)。
確認 webroot 之外的機密不會被其他使用者讀取
把機密移出 webroot 後,如果權限太寬鬆,同一台伺服器上的其他帳號仍能讀取,那也沒有用。find ~ -maxdepth 3 \( -name '.env*' -o -name '*.pem' -o -name '*token*.json' \) -perm /o+r 2>/dev/null 會列出擁有者以外的人也能讀取的機密;用 chmod 600 修正。沒有輸出就代表沒問題。
確認搜尋引擎是否已建立索引
在 Google 搜尋 site:yourdomain,找找有沒有你不認得的檔案(.sql、.zip、.txt 等)。如果你使用 Search Console,「網頁」報表會列出已建立索引的 URL。如果東西還在搜尋結果中,處理方式請見下方的常見錯誤。
發現時的處理
移除、撤銷、遷移 三件事要成套執行。少做任何一件,修正就不完整。
從 webroot 移除,讓 URL 取不到
撤銷/重新核發可能已外洩的金鑰或權杖
機密放在 webroot 之外,權限 600
依伺服器區分:拒絕提供這些檔案的設定(作為備援)
真正的修正是不要把檔案放在那裡。在此之上,再在 Web 伺服器加上拒絕規則,讓不小心放進去的東西仍然不會被提供。能用的方法依環境而定。
| 環境 | 拒絕方式 | 目錄清單 | 注意事項 |
|---|---|---|---|
| Apache(VPS) | 在 <Directory> 區塊或 .htaccess 中使用 <FilesMatch> + Require all denied | 用 Options -Indexes 關閉 | Apache 2.4 的 AllowOverride 預設為 None,除非你允許,否則 .htaccess 會被忽略 |
| Nginx(VPS) | 在 location 中使用 deny all; | autoindex 預設為 off | 不讀取 .htaccess。正規表示式的 location 依撰寫順序檢查,第一個符合的生效 |
| 共用主機 | 通常是 .htaccess(相容 Apache) | 透過控制台或 .htaccess | 用不含機密的測試檔案確認它確實生效 |
| 靜態主機/CDN | 輸出資料夾中的東西會原樣公開 | 依服務而定 | 依靠「不把機密放進輸出資料夾」,而不是拒絕規則 |
Apache 範例(已啟用 .htaccess 的環境):
Options -Indexes
<FilesMatch "(^\.|~$|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key)$)">
Require all denied
</FilesMatch>Nginx 範例(在 server 區塊內):
# Allow the path used for automatic certificate renewal (Let's Encrypt) first
location ^~ /.well-known/acme-challenge/ { }
# Deny files and directories starting with a dot (.env, .git, ...)
location ~ /\. { deny all; }
# Deny backups and archives
location ~* (~|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key))$ { deny all; }在 Nginx 中,請求符合標記為 ^~ 的 location 時,就不會再檢查正規表示式的 location。因此即使有拒絕以點開頭路徑的規則,憑證更新用的 /.well-known/acme-challenge/ 仍會被提供。修改設定後,用 nginx -t 檢查語法、重新載入,並確認測試檔案回傳 403。如果你的站點提供 ZIP 等檔案下載,請把該副檔名從拒絕清單中移除。
容易落入的放置方式(危險)
- 整合用的權杖 JSON 放在
public/ - 樣板裡帶著的機密檔案保持公開
- 備份(
.sql/.bak)或.git留在公開目錄底下 - 只修一個站點就說「處理完了」
正確的放置方式
- 公開目錄 只放可以公開的東西
- 機密放在 webroot 之外,權限 600
- 敏感副檔名 拒絕 提供
- 發現一個就 盤點所有主機與站點
把「只放可以公開的東西」定為固定規則
規則比逐次修補更有效。規定公開目錄只放可以公開的檔案,原則上絕不把機密、備份、版本控制資料(.git)或設定檔放在那裡。更新樣板時,要從範本本身移除機密,否則之後大量建置的每個站點都會繼續複製同一個機密檔案。
常見錯誤與修正方式
- 依賴拒絕規則,卻把檔案留在原處:搬遷伺服器或改寫設定時規則可能消失,檔案從那天起就公開了。Nginx 完全不讀
.htaccess。把規則當作備援,並把檔案移出公開目錄。 - 直接在正式伺服器上編輯檔案:編輯器會留下以
~或.swp結尾的備份。OWASP 測試指南也建議不要直接在 Web 伺服器上編輯檔案。請在本機編輯,透過部署送出變更。 - 把整個專案資料夾複製到公開目錄:
.env、.git和測試資料都會跟著出去。只提供建置輸出資料夾,並明確指定要複製的內容。 - 拒絕所有以點開頭的路徑,結果憑證更新失敗:Let's Encrypt 把驗證檔案放在
/.well-known/acme-challenge/底下。如上面的 Nginx 範例,先允許那個路徑。 - 把機密放在
VITE_或NEXT_PUBLIC_變數中:它們會被嵌入建置後的 JavaScript,任何人都讀得到。只在伺服器端程式碼中使用機密,並撤銷已經以這種方式送出的 key。 - 刪除檔案就停手:如果搜尋引擎已建立索引,它可能還留在結果中。確認 URL 回傳 404 或 410,然後用 Google Search Console 的「移除」工具暫時隱藏(約六個月),同時等待重新檢索把它清除。
實際案例:公開報告裡隱藏的個人資料
負責華盛頓特區 Medicaid(低收入者的公共醫療保險)的醫療保健財務部(DHCF)表示,2023 年至 2026 年 7 月間刊登在其網站上的兩份統計報告,以「隱藏欄位」形式包含了原始的個人資料。這些報告本來只顯示彙總數字,但檔案中仍保留了受益人的 Medicaid 號碼、出生日期等資訊。向美國衛生及公共服務部(HHS)提交的通報中,受影響人數為 399,086 人。DHCF 於 2026 年 7 月 21 日得知此問題,並移除了報告。
這不是遺忘機密檔案的案例,但根源相同:
- 公開目錄裡的檔案可以被完整取走,不只是畫面上顯示的部分
- 發布者以為「看不到」的資訊,在審查時最容易被漏掉
- 可能在被發現之前就已經刊登了好幾年
在發布前檢查「這個檔案裡實際有什麼」的步驟,能同時防止遺留檔案和隱藏資料。完整經過請見 華盛頓特區 Medicaid 資料外洩(英文)。
本站是怎麼做的
本站的基本做法是機密(金鑰、權杖、連線字串)既不放進公開目錄,也不放進程式碼儲存庫。部署產物只包含建置好的公開資源;機密則作為執行環境的環境變數,存放在不提供存取的地方。原因是:本文這類事故的典型情況,就是一個遺留檔案讓任何請求其 URL 的人取得全部內容。我們把放置位置本身設計成存取控制,並與 漏洞因應的盤點 以相同週期執行這項盤點。
接下來閱讀
- 依技術:共用伺服器上的 .env 保護 · 漏洞因應實務(盤點)
- 術語:.env 檔案與機密資訊 · 路徑穿越
- 事故:.env 被整個公開的案例
- 保管:機密的安全保管
- 雲端:公開的 bucket(英文,伺服器之外的同一個問題)
- 主機業者被入侵時:網站主機遭入侵時該怎麼做(英文,假設放在那裡的東西都已被讀取)
- 實際案例:留在公開報告中的 Medicaid 號碼(華盛頓特區)(英文,公開的檔案被完整取走)
參考資料
- IPA, "安全なウェブサイト運営にむけて/20 Checkpoints for Secure Website Operation and Management"(日文;不應公開的檔案、搜尋引擎快取):ipa.go.jp
- OWASP, "Web Security Testing Guide: Review Old Backup and Unreferenced Files for Sensitive Information"(.old、~、.swp 備份;不要直接在伺服器上編輯):wstg.owasp.org
- Apache HTTP Server 2.4, "Upgrading to 2.4 from 2.2"(
AllowOverride預設為None)與 "mod_autoindex"(Options +Indexes會產生清單):httpd.apache.org, httpd.apache.org - nginx, "ngx_http_core_module: location"(比對順序與
^~)與 "ngx_http_autoindex_module"(autoindex預設為off):nginx.org, nginx.org - Let's Encrypt, "Challenge Types"(HTTP-01 使用
/.well-known/acme-challenge/):letsencrypt.org - Vite, "Env Variables and Modes"(
VITE_變數會暴露給用戶端程式碼):vite.dev - Next.js, "Environment Variables"(
NEXT_PUBLIC_變數會被嵌入瀏覽器的 JavaScript):nextjs.org - Google Search Console 說明, "Removals tool"(暫時移除約持續六個月):support.google.com
- District of Columbia DHCF, "Notice of Data Incident":dhcf.dc.gov;HIPAA Journal(向 HHS 通報的人數):hipaajournal.com