Next.js 安全實踐與環境變數
安全性是 Web 應用開發中最重要卻常被忽視的一環。Next.js 雖然內建了許多安全性功能(如自動轉義內容以防 XSS),但在 App Router 架構下,Server Components 與 Server Actions 的混合使用帶來了新的挑戰。
本篇將深入探討如何構建一個「預設安全 (Secure by Default)」的 Next.js 應用。
環境變數的安全管理
環境變數是將機密資訊(如 API Key, DB Password)與程式碼分離的標準做法。
變數分級原則
Next.js 透過前綴詞區分變數的可見範圍:
- 私有變數 (Private):無前綴(例如
DATABASE_URL)。僅在伺服器端可用。若在 Client Component 引用會回傳undefined,Next.js 編譯時也會發出警告。 - 公開變數 (Public):以
NEXT_PUBLIC_開頭(例如NEXT_PUBLIC_ANALYTICS_ID)。這些值會被打包進 JavaScript bundle 中,任何人都看得到。
強制驗證 (Type-safe Environment Variables)
為了避免程式在執行時因為缺少環境變數而崩潰,推薦使用 zod 在 build time 進行驗證。可以建立一個 src/env.mjs:
// src/env.mjs
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
NEXT_PUBLIC_API_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'test', 'production']),
});
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error('❌ Invalid environment variables:', parsed.error.format());
process.exit(1);
}
export const env = parsed.data;
這樣你就能確保應用程式啟動時,所有設定都是正確的。
Server Actions 的安全性
Server Actions 本質上就是一個公開的 POST API endpoint。因此,絕對不能相信客戶端的輸入。
驗證與權限檢查模式
每個 Action 都應該包含三個步驟:權限驗證 (Authentication) -> 資料驗證 (Validation) -> 授權 (Authorization)。
'use server';
import { z } from 'zod';
import { auth } from '@/auth'; // 假設使用 Auth.js
import { db } from '@/lib/db';
const schema = z.object({
title: z.string().min(1),
});
export async function createPost(formData: FormData) {
// 1. 驗證登入 (Authentication)
const session = await auth();
if (!session?.user) {
throw new Error('Unauthorized');
}
// 2. 驗證資料格式 (Validation)
const parsed = schema.safeParse({
title: formData.get('title'),
});
if (!parsed.success) {
return { error: 'Invalid fields' };
}
// 3. 業務邏輯與權限 (Authorization)
// 確保使用者只能建立屬於自己的文章
await db.post.create({
data: {
title: parsed.data.title,
authorId: session.user.id,
},
});
}
防止資料洩漏:Taint APIs
在 React Server Components 中,我們會將物件從 Server 傳遞給 Client Component。為了防止意外將整個 User 物件(包含 password_hash 等欄位)傳送出去,Next.js 提供了 Taint APIs。
experimental_taintObjectReference
標記特定物件實例為「受汙染」,禁止傳遞給 Client。
import { experimental_taintObjectReference } from 'react';
export async function getUser(id: string) {
const user = await db.user.findUnique({ where: { id } });
if (user) {
experimental_taintObjectReference(
'Do not pass the entire user object to the client. Pick specific fields.',
user
);
}
return user;
}
如果某個開發者試圖將 user 直接 prop 給 Client Component:
// ❌ 這會導致錯誤
<UserProfile user={user} />
內容安全策略 (Content Security Policy - CSP)
CSP 是防禦 XSS (跨站腳本攻擊) 的強大防線。它限制了瀏覽器可以載入哪些資源(Scripts, Styles, Images)。
在 Next.js 16+ 中,最適合實作 CSP 的地方是 src/proxy.ts。
// src/proxy.ts
import { nextProxy } from 'next/server';
export default nextProxy({
async handle(request) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
// 允許來自同源的腳本,以及帶有正確 nonce 的內聯腳本
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
block-all-mixed-content;
upgrade-insecure-requests;
`
.replace(/\s{2,}/g, ' ')
.trim();
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce); // 讓 Server Component 也能拿到 nonce
requestHeaders.set('Content-Security-Policy', cspHeader);
// 1. 將帶有 CSP 的 Request 往後送
const response = await nextProxy.next({
request: {
headers: requestHeaders,
},
});
// 2. 確保 Response Header 也包含 CSP 指令
response.headers.set('Content-Security-Policy', cspHeader);
return response;
},
});
Server-Only 依賴隔離
為了確保後端邏輯程式碼(如資料庫連線、私鑰簽署)永遠不會被打包到客戶端 Bundle 中,請使用 server-only 套件。
npm install server-only
在所有敏感檔案頂部加入:
// lib/data.ts
import 'server-only';
export async function getData() {
// ...
}
小結
- 環境變數:使用 Zod 進行強型別驗證,嚴格區分 Public/Private。
- Server Actions:永遠預設為不可信,需驗證身分與輸入資料。
- 資料保護:使用 Taint API 防止敏感物件外洩。
- 防禦縱深:配置 CSP Header 與使用
server-only隔離程式碼。
建立安全的應用程式需要層層把關,利用 Next.js 提供的這些工具,可以大幅降低常見的安全風險。