Next.jsはフロントエンドとサーバー処理を同じコードベースで扱えるフレームワークですが、その柔軟さゆえに「どこまでがブラウザに公開され、どこからがサーバー内部の処理か」を開発者自身が意識的に設計しないと、情報漏えいや認可漏れといった事故につながりやすい構造でもあります。本記事では、要件定義からリリース、運用フェーズまでを通しで使える「Next.jsセキュリティ対策チェックリスト」を、カテゴリ別に実装コード付きで整理しました。チェックリストをそのまま社内ドキュメントやPRテンプレートに転用できる形でまとめていますので、開発チーム・情シスの双方でご活用ください。
なぜNext.jsで「作ってから気づく」事故が起きやすいのか
Next.jsはApp Router以降、Server Components・Server Actions・Middlewareなど、サーバー側で動く仕組みが増えました。これは実装の自由度が上がった一方で、次のような判断を都度エンジニアに委ねる設計でもあります。
- このデータはサーバーだけで完結させるのか、クライアントに渡すのか
- このページは静的生成(SSG)してよいのか、リクエストごとにサーバーで生成(SSR)すべきか
- この環境変数は
NEXT_PUBLIC_を付けてよいのか - このAPI Route・Server Actionは誰が呼んでも良いのか
これらの判断を個々の開発者の裁量に任せたままにすると、機能追加のたびに攻撃面(アタックサーフェス)が静かに広がっていきます。チェックリストの目的は、こうした判断を「毎回ゼロから考える」のではなく「決まった型に沿って確認する」ことで、レビューの抜け漏れと属人化を防ぐことにあります。
チェックリストの運用設計:優先度とタイミングを決める
項目を並べるだけのチェックリストは形骸化します。実務で機能させるには、優先度とチェックタイミングをセットで決めておくことが重要です。
優先度は3段階で管理する
- 必須(Must):未達なら本番公開不可。認証・認可の欠落、シークレットの露出、既知のCritical脆弱性放置など
- 推奨(Should):公開は可能だが、期限を切って是正する。セキュリティヘッダー不足、ログ設計の不備など
- 改善(Nice to have):余力があれば強化する。SAST/DAST導入、脅威モデリング、ペネトレーションテストなど
チェックのタイミングを開発フローに埋め込む
- 設計時:認証方式・権限モデル・データ分類・外部連携先の洗い出し
- 実装中(PR単位):入力検証の有無、権限チェックの有無、シークレットのハードコードがないか
- CI:依存関係の脆弱性スキャン、Lint、型チェック、シークレット検知
- リリース前:セキュリティヘッダー、環境変数の公開範囲、ビルド成果物の中身
- 運用中:月次の依存関係アップデート、ログ監視、インシデント訓練
以降、各カテゴリのチェック項目と実装例を具体的に示します。
カテゴリ1:認証・認可
認証で確認すべき項目
- パスワード認証を自前実装せず、Auth.js(NextAuth)やClerk、Firebase Authなど実績のある基盤に寄せているか
- 多要素認証(MFA)を有効化できる設計になっているか
- ログイン失敗時に「メールアドレスが存在しません」等、アカウントの有無が推測できるメッセージを返していないか
- 一定回数の失敗でレート制限・一時ロックがかかるか
認可で確認すべき項目
Next.jsで最も見落とされやすいのが「画面上は非表示にしているが、APIやServer Actionを直接叩けば取得・実行できてしまう」パターンです。認可判定は必ずサーバー側の最終防衛ラインとして実装します。
// app/api/orders/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getSessionUser } from "@/lib/auth";
import { getOrderById } from "@/lib/db/orders";
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await getSessionUser(req);
if (!user) {
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
}
const order = await getOrderById(params.id);
// ここが認可チェックの本体。
// 「ログイン済みか」だけでなく「この注文の所有者か」を必ず確認する
if (!order || order.userId !== user.id) {
return NextResponse.json({ error: "forbidden" }, { status: 403 });
}
return NextResponse.json(order);
}
URLやパラメータのIDを他人のものに書き換えても情報が取得できてしまう不備(いわゆるIDOR)は、Next.jsに限らず最も多い実害の一つです。「所有者チェックがサーバー側にあるか」を必須項目に加えてください。
カテゴリ2:入力検証とXSS/インジェクション対策
サーバー側バリデーションを必ず用意する
クライアント側のフォームバリデーションはUX目的であり、セキュリティの防御にはなりません。Server ActionsやAPI Routesの入り口では、Zodなどのスキーマバリデーションで型・形式・長さを必ず検証します。
// app/actions/createComment.ts
"use server";
import { z } from "zod";
import { getSessionUser } from "@/lib/auth";
import { insertComment } from "@/lib/db/comments";
const CommentSchema = z.object({
postId: z.string().uuid(),
body: z.string().min(1).max(2000),
});
export async function createComment(input: unknown) {
const user = await getSessionUser();
if (!user) throw new Error("unauthorized");
// 想定外の形式・巨大ペイロードはここで弾く
const parsed = CommentSchema.safeParse(input);
if (!parsed.success) {
throw new Error("invalid input");
}
return insertComment({ ...parsed.data, userId: user.id });
}
XSS対策:dangerouslySetInnerHTMLは例外扱いにする
Reactは基本的に出力時に自動エスケープされるため素の状態ではXSSが起きにくい設計ですが、Markdown変換やWYSIWYGエディタの出力をdangerouslySetInnerHTMLで描画する箇所は例外です。DOMPurifyなどでサニタイズを必ず通します。
import DOMPurify from "isomorphic-dompurify";
function ArticleBody({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ["p", "b", "i", "a", "ul", "li", "strong", "em"],
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
SSRF対策
ユーザーが指定したURLをサーバー側からfetchする機能(画像取得、OGP取得など)は、内部ネットワークやクラウドのメタデータエンドポイントへアクセスされるSSRFの温床になります。許可ドメインのホワイトリスト方式にし、プライベートIPアドレス帯へのリクエストは明示的に拒否してください。
カテゴリ3:通信・セッション/Cookie
- CookieにはSecure(HTTPSのみ送信)、HttpOnly(JSから参照不可)、SameSite=Lax以上を必ず設定する
- セッションの有効期限を適切に設定し、端末紛失時に強制ログアウトできる仕組みを用意する
- Cookie認証を使う場合、状態変更を伴う操作にはCSRF対策(トークン検証またはSameSite=Strict)を入れる
// Server ActionやRoute HandlerでのCookie発行例
import { cookies } from "next/headers";
cookies().set("session", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 8, // 8時間
});
カテゴリ4:Next.js固有の落とし穴
SSR/SSG/ISRのキャッシュと情報漏えい
ログイン後にしか見えないはずの個別データを、誤って静的生成(SSG)や共有キャッシュの対象にしてしまうと、別のユーザーに同じキャッシュが配信される事故が起きます。個人情報を含むページは原則SSR、もしくはcache: "no-store"を明示してキャッシュ対象から外します。
// 個別情報を扱うfetchはキャッシュさせない
const res = await fetch(`${API_BASE}/me/orders`, {
cache: "no-store",
headers: { Authorization: `Bearer ${token}` },
});
Middlewareの認証ガード漏れ
Middlewareで一括認証をかけている場合、matcherの設定漏れで一部の管理画面やAPIパスがガード対象外になっていないかを必ず確認します。
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(req: NextRequest) {
const session = req.cookies.get("session");
if (!session) {
return NextResponse.redirect(new URL("/login", req.url));
}
return NextResponse.next();
}
export const config = {
// /admin配下・/api/internal配下を漏れなく含める
matcher: ["/admin/:path*", "/api/internal/:path*", "/dashboard/:path*"],
};
オープンリダイレクトの防止
ログイン後の遷移先などをクエリパラメータで受け取る実装は、外部URLへのリダイレクトに悪用されフィッシングの踏み台になります。相対パスかつ許可されたパスのみを許容してください。
カテゴリ5:依存関係管理
Next.js本体・React・周辺npmパッケージは更新頻度が高く、既知の脆弱性が公開されてから悪用されるまでの時間も短くなっています。「作った時点で安全」ではなく、継続的に安全な状態を維持する仕組みが必要です。
# CIでの依存関係スキャン例(GitHub Actions)
name: dependency-audit
on: [pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm audit --audit-level=high
- Dependabotなどで自動的に脆弱性のあるパッケージのPRを検知する
- Critical/Highは原則○日以内にアップデートする社内SLAを決める
- lockファイル(package-lock.json等)を必ずコミットし、意図しないバージョン変動を防ぐ
- 使っていないパッケージは定期的に棚卸しして削除する
カテゴリ6:環境変数・シークレット管理
Next.jsのトラブルで最も多いのが、NEXT_PUBLIC_プレフィックスの誤解によるシークレットの露出です。このプレフィックスが付いた環境変数はビルド時にクライアントバンドルへ埋め込まれ、誰でもブラウザの開発者ツールから読み取れる「公開情報」になります。
// .env.example
# クライアントに公開してよい値のみ NEXT_PUBLIC_ を付ける
NEXT_PUBLIC_API_BASE_URL=https://api.example.com
# サーバーのみで使う値。絶対にNEXT_PUBLIC_を付けない
DATABASE_URL=postgres://...
STRIPE_SECRET_KEY=sk_live_...
JWT_SIGNING_SECRET=...
- シークレットはSecrets Manager等で管理し、リポジトリやチャットに平文で置かない
- APIキーは漏えいを前提に定期ローテーションする
- コミット前後でシークレット検知ツール(gitleaks等)を通す
- ビルド成果物(
.nextフォルダ等)に.envや社内ドキュメントが含まれていないか、デプロイ対象パスを明文化する
カテゴリ7:セキュリティヘッダー・配信
アプリの実装内容に依存せず一律に効果を出せるのが、レスポンスヘッダーによる防御です。next.config.jsで一括設定できます。
// next.config.js
module.exports = {
async headers() {
return [
{
source: "/:path*",
headers: [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
{
key: "Content-Security-Policy",
value:
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;",
},
],
},
];
},
};
CSPはいきなり厳格に設定すると画面崩れが起きやすいため、まずはレポート専用モード(Content-Security-Policy-Report-Only)で違反を観測し、段階的に絞り込む運用が現実的です。あわせてHTTPS強制、TLS証明書の自動更新、DNSレジストラの2FA、ログイン・検索・CSV出力などのAPIへのレート制限も基本チェック項目に含めます。
カテゴリ8:運用体制とインシデント対応
セキュリティは「対策を入れたか」より「運用で維持できているか」で差が出ます。特にNext.jsは更新頻度が高いため、以下を最低限のRunbookとして用意してください。
- 重大度別SLA:Critical即日〜数日、High 1〜2週間、Medium 次回定例リリース、Lowは棚卸しで整理
- 初動対応:連絡網、権限者、外部公開停止の手順(メンテナンス表示等)
- 封じ込め:APIキー失効、パスワードリセット、該当機能の一時停止
- 調査:参照すべきログの種類、保全方法、時系列の整理手順
- 再発防止:チェックリストへの反映、テストの追加、レビュー体制の見直し
まとめ:チェックリスト早見表
ここまでの内容を、そのままPRテンプレートや棚卸しシートに転用できる形で整理します。
- 【認証・認可】サーバー側で所有者・権限を必ず検証しているか
- 【入力検証】Server Actions/API Routesの入口でスキーマバリデーションを通しているか
- 【XSS】dangerouslySetInnerHTMLの箇所はサニタイズ済みか
- 【Cookie】Secure・HttpOnly・SameSiteが設定されているか
- 【キャッシュ】個人情報を含むページがSSG/共有キャッシュの対象になっていないか
- 【Middleware】認証ガードのmatcherに抜けているパスはないか
- 【依存関係】CIでの脆弱性スキャンとアップデートSLAが機能しているか
- 【環境変数】NEXT_PUBLIC_の付与基準が開発標準として明文化されているか
- 【ヘッダー】CSP・X-Content-Type-Options等が全ルートに適用されているか
- 【運用】重大度別のSLAとインシデントRunbookが用意されているか
Next.jsはハイブリッドなレンダリング方式や便利なServer Actionsのおかげで開発速度を大きく上げられる一方、その柔軟さの分だけ「サーバー側で最終判断しているか」を常に問い続ける必要があります。本チェックリストを起点に、自社のプロジェクトに合わせた項目の追加・優先度の調整を行い、開発フローの中に組み込んでいくことをおすすめします。

コメント