Next.js 安全實踐與環境變數

安全性是 Web 應用開發中最重要卻常被忽視的一環。Next.js 雖然內建了許多安全性功能(如自動轉義內容以防 XSS),但在 App Router 架構下,Server Components 與 Server Actions 的混合使用帶來了新的挑戰。

本篇將深入探討如何構建一個「預設安全 (Secure by Default)」的 Next.js 應用。

環境變數的安全管理

環境變數是將機密資訊(如 API Key, DB Password)與程式碼分離的標準做法。

變數分級原則

Next.js 透過前綴詞區分變數的可見範圍:

  1. 私有變數 (Private):無前綴(例如 DATABASE_URL)。僅在伺服器端可用。若在 Client Component 引用會回傳 undefined,Next.js 編譯時也會發出警告。
  2. 公開變數 (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 提供的這些工具,可以大幅降低常見的安全風險。