2026年5月15日8 分鐘Next.js · 資安強化 · CSP · Docker · Production Hardening · OWASP · Web 開發 · Apache · honeypot

Next.js 16 上線前的 5 個資安必做 — 從「能跑」到「廠商資安檢查能過」

我們最近做完一輪 gomylife.tw 的資安強化 — 從廠商資安檢查報告的「補強等級」5 個項目全部處理掉。這篇拆完整流程:升 Next.js 修 13 個 CVE、關掉框架洩漏、Docker base image 換 Debian、Apache 加 CSP、contact form 加 honeypot。一條一條都有踩過的坑跟解法。

陳先生 (Henry)

「我們做完一輪資安檢查,廠商給了一份『高/中/低優先』的清單。最高優先的 5 項全做完,總共花 2-3 小時。」

這是上週 gomylife.tw(我們自家公司形象站)的真實情況。網站跑在 Next.js 16 + Apache reverse proxy + Docker,公司形象站 + 沒登入 + 沒金流,原本以為攻擊面很小。但廠商資安檢查還是抓出 5 個值得修的項目。

這篇把 5 個項目跟我們踩到的具體坑寫出來。給其他用 Next.js 做公司網站的人參考。

我們的環境 #

  • 框架:Next.js 16 App Router
  • 部署:Docker container 在內網 host
  • 對外:Apache 2.4 reverse proxy + Let's Encrypt HTTPS
  • CDN:沒用(直接 server)
  • 登入功能:無
  • 金流:無
  • API:只有 /api/contact(聯絡表單) + /api/chat(AI 對話 widget)

攻擊面很小,但廠商照樣抓到下面 5 項。

1. Next.js 升到 16.2.6 — 修 13 個 CVE #

package.json 鎖在 16.2.4,廠商一查就跳出 13 個官方公告漏洞(版本範圍 9.3.4-canary ~ 16.3.0-canary 都中招):

漏洞類別 對我們影響
Middleware / Proxy bypass 沒用 middleware 認證,N/A
Cache poisoning 公開站台會被波及
App Router CSP nonce XSS 之後加 CSP 後就有影響
Image Optimization API DoS next/image,有
WebSocket SSRF 沒用 ws,N/A

修法很單純:

npm install next@16.2.6
npm install eslint-config-next@16.2.6  # 順手同步

這一升立刻踩到下一個坑 — Docker build fail。詳見下節。

2. Docker base image:Alpine → Debian slim(Next.js 16 必要) #

升完 Next.js 16 跑 docker compose up -d --build 直接 fail:

Error: Cannot find module '../lightningcss.linux-x64-musl.node'
Require stack:
- /app/node_modules/lightningcss/node/index.js

Root cause:Next.js 16 的 Turbopack 改用 lightningcss 處理 CSS,這套需要 platform-specific native binary。Alpine 用 musl libc、Ubuntu/Debian 用 glibc,binary 不相容

我們的 source-of-truth 在 Ubuntu dev server 上跑 npm install,產生的 lockfile 只鎖了 glibc 版的 lightningcss-linux-x64-gnu,沒鎖 musl 版。Docker 用 alpine base 時就找不到對應 binary。

修法:base image 從 node:22-alpine 換成 node:22-slim(Debian glibc):

# 改前
FROM node:22-alpine AS deps
# ...
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001

# 改後
FROM node:22-slim AS deps
# ...
RUN groupadd -g 1001 -r nodejs && useradd -u 1001 -r -g nodejs nextjs

注意:

  • 三個 stage(deps / builder / runner)都要改
  • Alpine 用 addgroup/adduser(busybox 語法),Debian 用 groupadd/useradd(shadow-utils)
  • Image size 從 ~180MB 增加到 ~280MB — 換來的是相容性 + 之後所有 native binary 套件不會踩雷

結論:Next.js 16+ 之後 alpine 應該不再是預設選擇。Debian slim 才是新王道。

3. 關掉 X-Powered-By: Next.js(一行設定) #

Next.js 預設 response header 會送 X-Powered-By: Next.js — 等於主動告訴攻擊者「我用什麼框架、來鎖定 exploit」。

修法一行:

// next.config.ts
const nextConfig: NextConfig = {
  output: "standalone",
  poweredByHeader: false,  // ← 加這行
  // ...
};

驗證:

curl -sI https://www.gomylife.tw/ | grep -i x-powered
# 沒輸出 = OK

低工 / 高回報的設定。Next.js 預設不該開,但既然開了我們得自己關。

4. Apache vhost 加 Content-Security-Policy #

CSP 是 Mozilla Observatory / 各家資安評估報告的必扣分項。我們其他 security header(HSTS / X-Frame-Options / Referrer-Policy / Permissions-Policy / X-Content-Type-Options)都有,就缺這個。

但 CSP 是會「改完立刻 break 整站」的設定 — 太嚴 = GA 不送 beacon、Google Fonts 不載、shadcn UI 樣式失效。

我們在 Apache vhost 直接加的可運作版本(涵蓋 GA4 + Tag Manager + Google Fonts):

Header always set Content-Security-Policy "default-src 'self'; \
  script-src 'self' 'unsafe-inline' \
    https://www.googletagmanager.com \
    https://www.google-analytics.com; \
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
  font-src 'self' https://fonts.gstatic.com data:; \
  img-src 'self' data: https:; \
  connect-src 'self' \
    https://www.google-analytics.com \
    https://*.google-analytics.com \
    https://*.analytics.google.com \
    https://www.googletagmanager.com; \
  frame-ancestors 'self'; \
  base-uri 'self'; \
  form-action 'self'"

幾個關鍵 insight:

  • 'unsafe-inline' 為什麼還在:Next.js inline 不少 hydration script + critical CSS。要拿掉得改用 nonce 模式(middleware 注入),那是大工程
  • font-src 包了 fonts.gstatic.com 但其實用不到:Next.js 用 next/font/google 引入 Google Fonts 時,字型檔是 build-time download 到本機 /.next/static/media/...woff2,runtime 是 self URL — 不接 fonts.gstatic.com。我們保留 external font-src 比較保險
  • connect-src 一定要包 *.google-analytics.com + *.analytics.google.com:GA4 的 beacon endpoint 是不固定 subdomain,wildcard 必要
  • frame-ancestors 'self' 取代舊的 X-Frame-Options(兩個都加也 OK,新瀏覽器看 frame-ancestors)

驗證流程:

# 1. header 真的送出了
curl -skI https://www.gomylife.tw/ | grep -i content-security

# 2. 開 Chrome devtools / Playwright,看 console 有沒有 CSP violation
# 3. GA Realtime dashboard 看得到自己 = 沒擋到 beacon

5. Contact form 加 honeypot(防 bot 灌水) #

我們的 /api/contact 原本只靠 IP rate limit(10/min + 100/day)。問題:攻擊者用 100 個 IP 就能灌 1 萬筆假 contact 進來

選擇:加 Cloudflare Turnstile / Google reCAPTCHA v3 — 但需要申請 site key + 前端 widget 整合。先用 honeypot 擋 80% 的 bot,之後流量爆量再上 Turnstile。

前端:加隱藏欄位 #

人類看不見(視覺 + tabindex + aria 都隱藏),但 bot 會解析 form 把它填上:

<form onSubmit={handleSubmit}>
  {/* Honeypot — 真人不會看到、tab 跳不到、screen reader 忽略
      ⚠️ 注意:實際部署時欄位名 *不要*用 "website" 這種公開範例名,
      請用獨特的、最好定期輪動的命名,避免被 bot 寫死規則繞過 */}
  <div
    aria-hidden
    className="absolute -left-[10000px] top-0 size-px overflow-hidden"
  >
    <label>
      [Your hidden field label]
      <input
        type="text"
        name="[your-unique-field-name]"  /* ← 換成你自己的命名 */
        tabIndex={-1}
        autoComplete="off"
        defaultValue=""
      />
    </label>
  </div>

  {/* 正常欄位 */}
  <input name="name" />
  <input name="email" />
  <textarea name="message" />
  ...
</form>

後端:填了就靜默 return 200 #

// Honeypot:真人填不到的隱藏欄位。bot 填了 → 靜默 return 200
// (讓 bot 以為成功不再 retry。不要回 400 / 403,bot 會換策略)
if (asStr(body[HONEYPOT_FIELD])) {
  console.log(
    `[contact] honeypot triggered from ${ip} ua="${userAgent}"`
  );
  return Response.json({ ok: true });
}

為什麼回 200 而不是 400:bot 收到 400 會調整策略再試(或換 IP)。回 200 + 不存檔,bot 以為成功,不會回頭重試 — 攻擊資源浪費掉。

Honeypot OPSEC 注意 #

  • 欄位名不要公開(包含這篇 blog 沒寫實際用什麼名字)
  • 進階做法:server-side render 時用 hash / session 產生欄位名,bot 沒辦法寫死規則
  • 更進階:搭配「填寫時間」判斷(人類不會 0.1 秒內填完表單,bot 會) → 雙保險
  • 監控 log:上線後實際看哪些 IP / UA 被擋,定期檢視 → 確認沒誤殺 + 看攻擊趨勢

驗證 #

# Bot 情境(填 honeypot 欄位)
curl -X POST https://your-site/api/contact \
  -H 'content-type: application/json' \
  -d '{"name":"Bot","email":"x@x","message":"spam","[hidden-field]":"http://spam.com"}'
# 回 {"ok":true},但 log 顯示 honeypot triggered + 沒存檔

# 真人情境(不填 honeypot)
curl -X POST https://your-site/api/contact \
  -H 'content-type: application/json' \
  -d '{"name":"Real","email":"real@example.com","message":"test"}'
# 回 {"ok":true},log 顯示 saved + 存檔

實測:上線當下沒多久就抓到第一隻 bot(自動化工具 UA + 海外 IP)。

完成 checklist #

做完上面 5 件事的驗證點:

  • npm audit 0 high vulnerabilities
  • curl -IX-Powered-By header
  • curl -IContent-Security-Policy header
  • 瀏覽器 console 0 CSP violations
  • GA Realtime dashboard 有看到自己(沒被 CSP 擋掉 beacon)
  • Docker build 成功(lightningcss musl issue 修掉)
  • Container healthcheck (healthy)
  • Contact form 模擬 bot(填 honeypot)回 200 但沒存檔
  • Contact form 模擬真人(空 honeypot)回 200 且存檔

我們刻意沒做的(Phase 3 之後) #

項目 為什麼先不做
Cloudflare Turnstile / reCAPTCHA honeypot 應該擋掉大部分 bot 了,等流量爆量再加
CSP 換 nonce 模式(去 unsafe-inline Next.js middleware 注入 nonce 是中等工程,先有 baseline CSP 比較重要
HSTS preload 申請 不可逆操作,確認所有 subdomain 都 HTTPS-only 再做
把 Anthropic API spend cap 加進來 我們有 max_tokens=1024 + max_turns=1 兩個 cost cap,先看實際攻擊量

重點總結 #

# 項目 風險
1 Next.js 升到 16.2.6 5 分鐘 低(升小版號)
2 Docker base alpine → slim 30 分鐘 低(image 大 100MB 而已)
3 poweredByHeader: false 1 分鐘 低(一行設定)
4 Apache vhost 加 CSP 30 分鐘 中(要驗證 GA + Fonts 沒破)
5 Contact form honeypot 20 分鐘 低(純前後端 trick)

整套 2-3 小時做完,下次資安檢查報告這 5 項全綠

想做類似的資安強化 #

如果你有 Next.js / 其他 Web 站要上線、或被資安檢查報告卡住,我們可以幫:

  • Code review + 強化清單 — 依現有 stack 給具體優先序
  • 整套部署 hardening — Apache vhost / Docker / CSP / rate limit 一條龍
  • OWASP Top 10 對照檢核 — 政府標案 / 金融客戶常要的合規檢查

聯絡:0912852835 / henryccy@icloud.com / LINE @3q3tw / 線上表單

延伸閱讀:

想聊類似的東西?

諮詢免費,依工時報價。

聯絡我們