我們把官網從 Next.js 15 升到 16:一次「不等出事」的主動防禦
資安維運
Ricky-
08 月 22,2026
Next.js資安維運升級實戰

只要網站掛在公開網域上,每天都被自動化工具掃描、試探漏洞——這不是誰特別倒楣,而是常態。這篇記錄我們為什麼決定不等出事,主動把 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 一顆知名漏洞的探針。

CVE-2025-29927:一個 header 就繞過所有權限檢查

2025 年 3 月,Next.js 被揭露一顆 CVSS 9.1 的高危漏洞 CVE-2025-29927。問題出在 middleware。

很多 Next.js 應用會把「登入驗證、權限判斷」寫在 middleware 裡——沒登入就擋在門外、不是管理員就導走。而框架內部用一個叫 x-middleware-subrequest 的 header 來避免 middleware 無限遞迴呼叫自己。漏洞在於:框架無條件信任這個 header。

middleware 驗證 / 權限關卡 受保護路由 /admin · /api 正常請求 ✕ 擋下 / 導走 偽造 header 的請求 x-middleware-subrequest: middleware 整包跳過 ✓ 直接進入
偽造一個 header,Next.js 就以為「這是內部子請求」,把 middleware 裡的登入與權限檢查整包略過。

於是你所有寫在 middleware 裡的驗證,全被繞過:後台、受保護的 API、只給付費會員看的內容,全都直接進得去。影響範圍是 Next.js 11.1.4 一路到 15.2.2,等於好幾年份的網站都中獎;官方在 15.2.3 修掉。

那我們為什麼還要升級?——因為漏洞不會只有一顆

這裡是重點。我們官網當時跑的是 15.2.4,剛好在 29927 修好之後——那顆我們躲過了。如果故事到這裡好像就沒事了,但真實世界不是這樣運作的:

15.2.4 之後陸續冒出、影響 15.x 的洞
  • 圖片優化快取洩漏(CVE-2025-57752):/_next/image 的快取金鑰沒把驗證 cookie 算進去,A 使用者的驗證內容被快取後,B 使用者不用登入也拿得到。
  • RSC 快取污染:共用快取的部署下,攻擊者可讓使用者拿到錯誤的頁面內容。
  • 反序列化導致的遠端程式碼執行(RCE):最嚴重的一類,可在伺服器上執行任意程式碼。
  • RSC 記憶體耗盡的阻斷服務(DoS)(CVE-2026-23864):一個構造過的請求就能讓伺服器卡在無限迴圈。
  • 2026 年 7 月,官方甚至一次性釋出修補 9 個 CVE 的安全更新。

看清楚這個節奏之後,結論就很明確:一個框架的漏洞是「持續冒出來」的,不是修完一顆就天下太平。

2025.03 CVE 陸續揭露 … 2026.07 29927 57752 RCE·DoS 9 CVE 停在 15.2.4 → 曝險持續累積 升 16.3.1 一次補齊
留在 15.2.4,等於停在一條不再收到安全更新的舊版本線上,讓曝險隨時間累積。

與其每冒出一顆洞就單點手動 patch、還要擔心漏掉,不如直接升到有持續維護、當前最新的版本線。所以我們升到了 Next.js 16.3.1——把過去一年累積的修補一次補齊,之後也繼續待在會收到安全更新的版本上。

帶得走的一句話

「主動防禦」不是等被打了才反應,而是把「跟上版本」變成日常維運的一部分。你躲過某一顆 CVE 不代表安全——真正的安全,是待在一條還在收到修補的版本線上。

升級實戰:我們踩到的雷

升大版本從來不是把數字改一改就好。以下是這次的完整過程與幾個坑。

1. 版本與環境

// 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 兩個階段都要換。

2. Turbopack 成為預設

Next 16 的 next build 預設走 Turbopack。好消息是編譯明顯變快;但它的解析器比舊的 webpack pipeline 更嚴格,會把一些「以前能過、其實不合規」的寫法直接擋下來——這就是下一個雷。

3. 最陰的一個雷:CSS @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";

這類「舊版本睜一隻眼閉一隻眼、新版本嚴格把關」的狀況,是升大版本最常見的雷。它其實是好事:這些寫法本來就不合規,只是以前沒被抓出來。

4. TypeScript 設定

Next 16 build 時會自動把 tsconfig.jsonjsxpreserve 改成 react-jsx(React 自動 runtime)。我們直接把這個變更寫進 repo,免得每次 build 都被自動改一次。

5. 產出物沒變

我們用 output: "standalone" 產出自包含的 server.js,正式映像裡不留原始碼——這點在 Next 16 完全沿用,不需要改。

驗證:不是 build 過就算數

升級這種事,「能 build」只是及格線。我們的驗收流程:

  1. 正式映像 buildnext build(Turbopack)完整編譯通過,所有路由正常產出。
  2. 實際起容器:首頁、內頁、API 端點全部回 200,資料正確渲染。
  3. 型別檢查tsc --noEmit 零錯誤。
  4. 確認產物:新內容確實打包進 bundle。

四關都過,才進到部署。

心法:把「安全」當成維運的日常,而不是意外時的救火

這次升級沒有驚天動地的故事,也正是重點所在——因為我們沒等到出事。網站被掃描、被試探是不會停的常態;框架的漏洞也會一顆接一顆冒出來。真正能長期把風險壓低的,不是某一次英雄式的搶修,而是把「跟上版本、定期升級、每次都驗證」變成流程裡固定會做的事。

在 Clelereve,我們替客戶維運線上服務時就是這樣做的:版本化部署、可一鍵回滾、健康監控,以及像這次一樣的主動升級。把上線後的安全與穩定當成產品的一部分——你的網站做好了只是開始,讓它一直安全地活著,才是我們的工作。

Tags:
Next.js資安維運升級實戰


相關文章
我當設計者,AI 當我的員工
我當設計者,AI 當我的員工
ricky - 2026-08-22T09:12:15.583064Z
把 AI coding agent 當員工的分工:我出交接規格、它實作、我審 diff 並獨立驗收,什麼該交出去、什麼要留在手上。
把流程寫進工具,而不是寫進腦袋
把流程寫進工具,而不是寫進腦袋
ricky - 2026-07-09T02:05:00Z
版本號手改的日子裡,我踩過三種坑,而它們都不是技術問題,是知識存放位置的問題。從自動化、預設值到「移除選項」,談談一人工作室為什麼也需要工程文化,以及工具擋不住的那一類錯。
別用 os 模組量容器資源
別用 os 模組量容器資源
ricky - 2026-08-21T10:42:34.774264Z
在容器裡呼叫 Node 的 os.totalmem() / os.cpus(),拿到的是宿主機的數字,不是你的容器。這篇談 namespace 與 cgroup 的差別、怎麼改用 process.cp...
訂閱電子報
隨時掌握最新的創業故事與設計開發過程
內容包含來自麒航團隊的日常分享、專業知識與工具推薦,助你了解更多創業背後的思維與技術。
麒航私房推薦
Clelereve Blog LogoClelereve Blog LogoClelereve Blog Logo
廣告區
Clelereve Blog Logo
聯絡我們
任何合作或問題請洽:
clelereve@gmail.com
Follow us
Copyright © 2025 blog.clelereve.com - All Rights Reserved
Frontend Version: -- | Backend Version: --
創業實驗筆記 - Clelereve Blog | 我們把官網從 Next.js 15 升到 16:一次「不等出事」的主動防禦 | Clelereve Blog