WordPress 上傳檔案安全最常被忽略的一點,是大家以為「後台不讓我傳 .php,就代表安全」。實際上,真正出事的入口幾乎都不在後台檔案總管,而在那些開放訪客上傳頭像、作品集、報名附件的外掛表單,以及一個被很多人放著不管的資料夾——wp-content/uploads。攻擊者不會傻傻地丟一個 shell.php 進來,他會把惡意程式碼藏進一張「看起來、驗起來都像真圖」的檔案裡,繞過你的檢查,最後在伺服器上跑起來。
這篇要談的不是「怎麼放寬上傳限制」,而是反過來:一張圖片到底能怎麼被武器化、WordPress 預設擋得住哪些、擋不住哪些,以及怎麼用三層防線把偽裝圖檔的惡意上傳真正堵死。內容同時涵蓋 Apache 與 Nginx,台灣中小型站台這兩種環境都很常見。
偽裝圖檔的惡意上傳是怎麼運作的
先講結論:攻擊的核心不是「副檔名造假」,而是讓同一個檔案「同時是合法圖片,又是可執行的程式碼」。這種檔案在資安領域叫 polyglot(多語格式檔),是繞過上傳檢查最常見的手法。
要理解它,得先知道伺服器怎麼判斷一個檔案是不是圖片。常見的判斷依據有三種,而每一種都有對應的破解方式。
第一種是看副檔名。最弱的防線,因為副檔名只是檔名後面那串字,改一下就好。於是有了雙重副檔名(double extension)的攻擊:把檔案命名成 avatar.php.jpg 或 avatar.jpg.php,碰到設定不嚴謹的伺服器,Apache 在某些 mod_mime 設定下會從右往左找到第一個「認識」的副檔名來決定處理方式,結果把 .php 當成可執行檔送進 PHP 直譯器。
第二種是看檔頭的魔術位元組(magic bytes)。每種檔案格式開頭都有固定的識別碼,例如 GIF 是 GIF89a、JPEG 是十六進位的 FF D8 FF。比較聰明的驗證會讀這幾個位元組來確認「這真的是圖嗎」。攻擊者的破法是:保留真正的圖片檔頭,把 PHP 程式碼塞在後面。檔案開頭仍是 GIF89a,驗證器看了點頭放行,但檔案裡同時藏著 <?php ... ?>。
第三種是讀 MIME type,也就是用 PHP 的 finfo 之類的函式去分析檔案內容、判定它是 image/jpeg 還是別的。這比看副檔名嚴謹得多,卻仍擋不住 polyglot——因為一個檔案可以「真的是一張合法 JPEG」,同時在 EXIF 中介資料的註解欄位裡藏 PHP 程式碼。攻擊者常用 ExifTool 這類工具,把 payload 寫進圖片的 Comment 欄位,再存成 .php。檔案內容確實通得過 finfo 的圖片判定,因為它本來就是張能正常顯示的圖。
把這三種破法串起來,攻擊路徑就清楚了:找到一個沒做好驗證的上傳表單,丟一個外觀是圖、骨子裡含 PHP 的檔案進 uploads,再用瀏覽器直接請求那個檔案的網址。只要伺服器願意把 uploads 裡的 .php 交給 PHP 執行,藏在圖裡的程式碼就跑起來,這就是所謂的 webshell(網頁後門),等於把整台伺服器的控制權交了出去。
WordPress 內建擋得住什麼,又漏了什麼
WordPress 核心其實有一定的自保能力,但它的保護範圍只到「透過官方上傳流程進來的檔案」為止。
核心會做兩件事。一是維護一份允許的 MIME 類型清單(白名單),你從媒體庫上傳時,副檔名與內容不在清單內就會看到「很抱歉,基於安全性考量,系統不接受這個檔案類型」這句熟悉的提示。二是自 WordPress 4.7.1 起,上傳時會用 wp_check_filetype_and_ext() 搭配伺服器的 finfo 去比對副檔名與實際內容是否相符,副檔名與內容對不上的就擋下。所以單純把 shell.php 改名成 shell.jpg 丟媒體庫,多半會被核心擋掉。
漏洞出在三個地方。
第一、外掛與佈景主題的自製上傳功能不一定走核心 API。表單外掛、作品集外掛、會員頭像、活動報名附件,只要開發者用自己寫的程式處理上傳、沒套用 WordPress 的驗證函式,核心那層白名單就形同虛設。歷年大量的上傳類漏洞都出在第三方外掛,而不是核心本身。
第二、finfo 驗證擋不住 polyglot。前一節說過,合法 JPEG 加 EXIF 藏碼這種檔案,內容判定就是 image/jpeg,finfo 沒理由拒絕。換句話說,內容驗證能擋掉笨拙的偽裝,但擋不住精心製作的多語格式檔。
第三、核心管不到「檔案進來之後會不會被執行」。WordPress 是應用層,它沒辦法決定 Apache 或 Nginx 要不要把 uploads 裡的 .php 拿去跑。這道關卡在伺服器設定,不在 WordPress 裡。
結論是:把希望全押在「WordPress 會幫我擋」並不安全。你需要的是一套不依賴單一環節的防線。
三層防線怎麼疊,才不會一破全破
最穩的作法是把防護拆成三層,彼此獨立、互為後援,任何一層失守,下一層還能接住。順序是:先管「誰能傳」,再管「傳了什麼」,最後管「傳進來的東西能不能跑」。
第一層是權限控管。在顯示上傳介面之前先確認使用者身分,不對匿名訪客開放上傳;真的需要開放(例如報名表附件),就把可上傳的角色、檔案大小、數量都收到最小範圍。能不開放上傳就不開放,是成本最低的一層。
第二層是內容驗證。在每一次上傳的處理程式裡,用 finfo_file() 讀出檔案真正的 MIME type,比對一份明確的白名單(例如只允許 image/jpeg、image/png、image/gif),不在清單內一律拒絕。同時擋掉雙重副檔名,並對檔名做正規化,避免 .php.jpg 這種混淆。這一層擋得掉絕大多數笨拙的偽裝。
第三層是伺服器層禁止執行。這是整套防線的底牌,也是最該優先做的一件事。直接在伺服器設定讓 uploads 目錄裡的任何 .php(以及其他腳本副檔名)都無法被執行。這樣即使前兩層全破、一個 polyglot 檔案真的躺進了 uploads,攻擊者用瀏覽器去請求它,伺服器也只會回傳 403 或把它當純文字,藏在裡面的 PHP 永遠不會被觸發。
這三層的關鍵在於「順序顛倒也沒關係,但缺一不可」。內容驗證再嚴,遇到 polyglot 還是會放行;這時候第三層的存在,讓「放行」不等於「被入侵」。下面分別講第二、三層的實際設定。
怎麼禁止 uploads 目錄執行 PHP(Apache 與 Nginx)
直接回答最多人問的兩個問題:禁止 uploads 執行 PHP 不會影響網站正常運作,因為這個資料夾本來就只該放圖片、影片、文件這類靜態媒體,沒有任何正常功能需要在裡面跑 PHP;而這個設定要寫在哪,取決於你的網站跑在 Apache 還是 Nginx。
如果是 Apache 環境(多數共享主機、cPanel 主機),在 wp-content/uploads/ 底下放一個 .htaccess,內容如下:
<Files *.php>
deny from all
</Files>
想更周全,把其他腳本副檔名一起擋,並關掉目錄瀏覽:
<FilesMatch ".(php|php3|php4|php5|php7|phtml|pl|py|jsp|asp|aspx|sh|cgi)$">
Require all denied
</FilesMatch>
Options -Indexes
Require all denied 是 Apache 2.4 之後的寫法;若主機仍是 2.2,用 Order deny,allow 與 Deny from all。放好之後,任何人直接請求 uploads 裡的 .php 都會吃到 403。
如果是 Nginx 環境(自架 VPS、OpenLiteSpeed、部分代管平台常見),.htaccess 完全沒作用,Nginx 不讀它。設定要寫進站台的 server 區塊(通常在 /etc/nginx/sites-available/ 的設定檔,或被主設定 include 的 .conf):
location ~* /wp-content/uploads/.*.php$ {
deny all;
}
改完別忘了測試設定語法再重載:先跑 nginx -t 確認沒有語法問題,再 systemctl reload nginx。Nginx 的好處是請求在進到 PHP-FPM 之前就被擋下,效率高。
兩種環境共通的注意點:少數外掛(特別是某些快取、最佳化外掛)會把產生的檔案丟進 uploads 的子目錄,極少數情況可能需要在規則裡開例外。設定完務必實測一輪——上傳一張正常圖片確認顯示無誤,再試著直接用網址請求一個 uploads 裡的 .php(可以自己丟一個無害的測試檔),看是不是回 403。能擋下,這層才算真的生效。
自己寫上傳功能時,內容驗證該怎麼做
如果你在開發佈景主題或外掛、需要自己處理檔案上傳,內容驗證這層就得親手寫好,不能假設核心會幫你擋。重點是「以實際內容為準,不以使用者提供的資訊為準」。
優先用 WordPress 內建函式而不是重造輪子。處理上傳時呼叫 wp_handle_upload(),它會套用核心的檔案類型檢查;要驗證副檔名與內容是否相符,用 wp_check_filetype_and_ext()。這兩個函式背後就會去比對 finfo 判定的 MIME 與宣稱的副檔名。
在這之上,自己再加一道明確的白名單比對。邏輯是:用 finfo_file() 讀出真實 MIME type,只接受你業務上真正需要的那幾種,其餘一律拒絕。例如一個只收頭像的表單,白名單就只放 image/jpeg 與 image/png,連 GIF 都不必開。白名單永遠比黑名單安全,因為你列得出「要什麼」,但永遠列不完「不要什麼」。
幾個容易漏掉的細節要一起做。檔名要重新產生、不要沿用使用者上傳的原始檔名,避免雙重副檔名與路徑穿越(path traversal)這類字串被帶進來。能對圖片做重新處理(re-encode)更好——把上傳的圖用 GD 或 Imagick 重新輸出成一張新圖,這個動作會把藏在 EXIF 或檔尾的 PHP payload 一起丟掉,是對付 polyglot 相當實際的一招。最後,上傳目錄的權限設好,目錄給 755、檔案給 644,不要為了省事開成 777。
要特別提醒 SVG。很多人為了放向量圖示而用 upload_mimes 過濾器開放 SVG 上傳,但 SVG 本質是 XML,可以內嵌 JavaScript,開放它等於開了一道跨站腳本(XSS)的門。真的需要 SVG,務必搭配會清洗(sanitize)內容的處理機制,把 SVG 裡的腳本與事件屬性洗掉再存,不要直接原樣放行。
用 upload_mimes 放寬類型前,先想清楚風險
upload_mimes 過濾器是 WordPress 提供來增刪允許上傳類型的官方介面,網路上大量教學教你用它「解除上傳限制」,但這件事要反過來想:每多開一種類型,就多一個要防的面向。
WordPress 預設不允許上傳某些類型,是經過安全權衡的結果,不是 bug。需要開放新類型(例如客戶要傳 PDF 報價單、或設計稿要收 WebP)當然可以,但開放的同時要問自己兩件事:這個類型有沒有可被內嵌腳本的風險(SVG、HTML 是高風險)、伺服器層的執行禁令有沒有把對應的腳本副檔名一起擋掉。
至於該不該用「禁止上傳某類型」的寫法(例如 unset($mimes['jpg']))來提升安全,意義不大——攻擊者根本不走你的後台白名單,他走的是沒做驗證的第三方表單。把力氣花在前面講的三層防線,遠比在 upload_mimes 裡加加減減有效。
安全外掛能不能一鍵搞定
可以幫你省事,但不能取代理解。主流安全外掛(例如 Sucuri、Wordfence 這類)大多在強化(hardening)選項裡提供「封鎖 uploads 目錄執行 PHP」的開關,按下去它就幫你部署對應的 .htaccess 規則,對共享主機或不想碰設定檔的使用者來說是很好的入口。這些外掛通常也附帶惡意檔案掃描,能定期巡 uploads 有沒有混進可疑的 .php。
但兩個前提要記得。外掛代你做的,本質還是前面講的那幾條規則,它幫你按按鈕,不代表你可以不知道它在做什麼;遇到 Nginx 環境,許多外掛的「一鍵硬化」其實寫的是 .htaccess,對 Nginx 無效,這種情況還是得手動改 server 設定。再來,外掛是輔助不是萬靈丹,第三方上傳表單的程式漏洞、過期沒更新的外掛本身,都不是「封鎖 PHP 執行」這個開關管得到的,定期更新與最小化外掛數量仍是基本功。
把外掛當成「快速部署伺服器層防線 + 持續掃描」的工具,而不是「裝了就不用管」的保險,方向才對。
WordPress 上傳檔案安全的核心,從來不是某一條神奇設定,而是承認「驗證一定有被繞過的一天」,並提前在伺服器層準備好那道讓惡意檔案就算進來也跑不起來的底牌。今天就先做三件事:確認你的站是 Apache 還是 Nginx,照對應寫法把 uploads 的 PHP 執行關掉並實測 403,再盤點站上所有開放訪客上傳的表單外掛、確認它們有沒有走核心驗證。把這三層疊起來,偽裝成圖片的惡意上傳,就只剩一張跑不動的圖。