只要網站掛在公開網域上,每天都被自動化工具掃描、試探漏洞——這不是誰特別倒楣,而是常態。這篇記錄我們為什麼決定不等出事,主動把 clelereve 官網從 Next.js 15 升到 16,以及升級過程踩到的雷。
打開任何一台對外伺服器的 access log,幾乎每天都會看到這種請求:
# 這些不是真人在逛,是掃描器在「批次試門把」 GET /.env GET /.git/config GET /wp-admin/setup-config.php GET / x-middleware-subrequest: middleware
它們把已知的漏洞、外洩的設定檔路徑、後台入口,對整個網際網路無差別地打一遍,命中一個就進一步利用。最後那一行特別值得注意——x-middleware-subrequest 這個 header,正是針對 Next.js 一顆知名漏洞的探針。
2025 年 3 月,Next.js 被揭露一顆 CVSS 9.1 的高危漏洞 CVE-2025-29927。問題出在 middleware。
很多 Next.js 應用會把「登入驗證、權限判斷」寫在 middleware 裡——沒登入就擋在門外、不是管理員就導走。而框架內部用一個叫 x-middleware-subrequest 的 header 來避免 middleware 無限遞迴呼叫自己。漏洞在於:框架無條件信任這個 header。
於是你所有寫在 middleware 裡的驗證,全被繞過:後台、受保護的 API、只給付費會員看的內容,全都直接進得去。影響範圍是 Next.js 11.1.4 一路到 15.2.2,等於好幾年份的網站都中獎;官方在 15.2.3 修掉。
這裡是重點。我們官網當時跑的是 15.2.4,剛好在 29927 修好之後——那顆我們躲過了。如果故事到這裡好像就沒事了,但真實世界不是這樣運作的:
/_next/image 的快取金鑰沒把驗證 cookie 算進去,A 使用者的驗證內容被快取後,B 使用者不用登入也拿得到。看清楚這個節奏之後,結論就很明確:一個框架的漏洞是「持續冒出來」的,不是修完一顆就天下太平。
與其每冒出一顆洞就單點手動 patch、還要擔心漏掉,不如直接升到有持續維護、當前最新的版本線。所以我們升到了 Next.js 16.3.1——把過去一年累積的修補一次補齊,之後也繼續待在會收到安全更新的版本上。
「主動防禦」不是等被打了才反應,而是把「跟上版本」變成日常維運的一部分。你躲過某一顆 CVE 不代表安全——真正的安全,是待在一條還在收到修補的版本線上。
升大版本從來不是把數字改一改就好。以下是這次的完整過程與幾個坑。
// package.json "next": "16.3.1", // was 15.2.4 "react": "^19.2.0", // Next 16 需要 React 19 "react-dom": "^19.2.0"
Next 16 的執行環境要求 Node.js ≥ 20.9(正式砍掉 Node 18)。我們順手把 Docker 映像從 node:20-alpine 拉到 node:22-alpine(現行 LTS),build 與 runtime 兩個階段都要換。
Next 16 的 next build 預設走 Turbopack。好消息是編譯明顯變快;但它的解析器比舊的 webpack pipeline 更嚴格,會把一些「以前能過、其實不合規」的寫法直接擋下來——這就是下一個雷。
@import 順序升級後開發模式一啟動就整站 500:
Error: Parsing CSS source code failed @import rules must precede all rules aside from @charset and @layer statements
指向 globals.css。我們的檔案開頭長這樣:
@import "tailwindcss"; @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk...');
看起來 @import 明明都在最上面?問題在於 Tailwind v4 的 @import "tailwindcss" 會被展開成一大段實際的 CSS 規則。展開之後,後面那行 Google Fonts 的 @import 就「跑到規則後面」了,違反 CSS 規範「@import 必須在所有規則之前」。舊版本容忍它,Turbopack 不容忍。修法很簡單——把字型 import 放到 Tailwind 之前:
/* 字型 @import 必須在所有規則之前 */ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk...'); @import "tailwindcss";
這類「舊版本睜一隻眼閉一隻眼、新版本嚴格把關」的狀況,是升大版本最常見的雷。它其實是好事:這些寫法本來就不合規,只是以前沒被抓出來。
Next 16 build 時會自動把 tsconfig.json 的 jsx 從 preserve 改成 react-jsx(React 自動 runtime)。我們直接把這個變更寫進 repo,免得每次 build 都被自動改一次。
我們用 output: "standalone" 產出自包含的 server.js,正式映像裡不留原始碼——這點在 Next 16 完全沿用,不需要改。
升級這種事,「能 build」只是及格線。我們的驗收流程:
next build(Turbopack)完整編譯通過,所有路由正常產出。tsc --noEmit 零錯誤。四關都過,才進到部署。
這次升級沒有驚天動地的故事,也正是重點所在——因為我們沒等到出事。網站被掃描、被試探是不會停的常態;框架的漏洞也會一顆接一顆冒出來。真正能長期把風險壓低的,不是某一次英雄式的搶修,而是把「跟上版本、定期升級、每次都驗證」變成流程裡固定會做的事。
在 Clelereve,我們替客戶維運線上服務時就是這樣做的:版本化部署、可一鍵回滾、健康監控,以及像這次一樣的主動升級。把上線後的安全與穩定當成產品的一部分——你的網站做好了只是開始,讓它一直安全地活著,才是我們的工作。





