Next.js API 認証とセキュリティ:JWT からレート制限まで完全実践ガイド
スマートフォンが震えました。クラウドプロバイダーからの請求通知——7,800 ドルです。
目をこすり、見間違いだと思いました。先月の請求は 120 ドルだけ。詳細を開くと、API 呼び出し回数は 1,800 万回。普段は 1 日数百回の個人プロジェクトです。
原因は、対策ゼロの API エンドポイントをクローラーに見つけられ、丸 3 日間叩き続けられたことでした。このとき初めて、API セキュリティは「あればいい」ではないと身をもって知りました。
本記事では、ここ数年で踏んだ地雷と検証した対策を体系的にまとめます。JWT 認証から CORS 設定、レート制限、入力検証まで——理論だけでなく、プロジェクトにそのまま使える実践コードを交えて解説します。
なぜ API セキュリティが重要なのか
API セキュリティに対する一般的な脅威
昨年 12 月、React 公式から重大なセキュリティ勧告が出ました。CVE-2025-55182、CVSS スコアは満点の 10.0 です。
これは何を意味するか。満点です。攻撃者が特殊な HTTP リクエストを送るだけで、サーバー上で任意のコードを実行できます。React Server Components を使っていて更新を怠っていれば、実質的に丸裸の状態です。
これだけではありません。今年 3 月には認可バイパス脆弱性 (CVE-2025-29927) も発見され、スコア 9.1。リクエストヘッダーを偽装するだけでミドルウェアの認証を回避できます。認証を設定したつもりでも、スキップされてしまうのです。
日常的な脅威も無視できません。
悪質なクローラーと DDoS 攻撃。レート制限のない API は一瞬でダウンします。ログイン API が 1 秒間に 3,000 回叩かれ、ブルートフォースでサーバーが落ちた事例も見ました。
データ漏洩。権限管理が不十分で、ユーザー A がユーザー B の注文情報を見れてしまう。ニュースになれば、ブランドの評判は地に落ちます。
インジェクション攻撃。SQL インジェクション、XSS、コマンドインジェクション……古臭く聞こえても、今も多数のプロジェクトが被害に遭います。「React が自動エスケープしてくれる」と思っていても、API 側で検証していなければ意味がありません。
Next.js API Routes の特徴
Next.js の API Routes は従来のバックエンドと少し異なり、次の点に注意が必要です。
Serverless ファースト。Vercel にデプロイすると、各 API リクエストは独立した Serverless 関数として実行されます。自動スケールは便利ですが、ステートレス——従来のメモリ内 Session は使えず、JWT か DB Session に切り替える必要があります。
フロントエンドとの混在。コードが同じリポジトリにあるため、環境変数が誤ってクライアントに漏れやすいです。DATABASE_URL を .env に入れたつもりが、バンドルに含まれ GitHub で公開されたケースも見ました。
エッジコンピューティングの制限。Edge Runtime を使う場合、一部の Node.js API が使えず、暗号化ライブラリや DB 接続の選び直しが必要です。そのときはセキュリティ設計も合わせて調整します。
要するに、Next.js API はあなたのバックエンドです。軽くて柔軟ですが、問題も起きやすい——そう理解しておきましょう。
API 認証の実践
認証方式の選び方
まず現実的な問題から。JWT と Session、どちらを使うべきでしょうか。
Next.js プロジェクトを始めた頃、私も悩みました。JWT は必須、Session のほうが安全——説は様々です。結局、シーン次第だと分かりました。
JWT が向いているケース:
- 複数サーバー(Serverless、エッジノード)にデプロイする
- クロスドメイン認証が必要(フロントが app.com、API が api.com など)
- Session ストアを管理したくない、できるだけシンプルにしたい
JWT の本質は、ユーザー情報をトークンにエンコードし、サーバー側で状態を持たないこと。リクエストごとにトークンを送ればよく、水平スケールが非常に楽です。
Session が向いているケース:
- サーバー側から能動的に制御したい(強制ログアウト、権限のリアルタイム変更)
- セキュリティ要件が極めて高く、クライアントにユーザー情報を持たせたくない
- Redis や DB があり、Session 管理が負担にならない
Session は状態がサーバー側にあり、クライアントは Session ID だけ。特定ユーザーを無効化したければ Session を削除すればよい——JWT ではこれができません。
私の選択基準:小規模・個人プロジェクトなら JWT で手早く。エンタープライズで細かい制御が必要なら Session。迷ったらまずどちらか一つで始め、必要になったら切り替えれば十分です。
JWT 認証の完全実装
JWT を選んだと仮定して、Next.js での実装を見ていきましょう。
ステップ 1:トークンの生成と検証
まずライブラリをインストールします。
npm install jose
jsonwebtoken ではなく jose なのは、Edge Runtime に対応しているからです。Web 標準の実装で、どの環境でも動きます。
lib/auth.ts を作成:
import { SignJWT, jwtVerify } from 'jose';
const secret = new TextEncoder().encode(
process.env.JWT_SECRET || 'your-secret-key-at-least-32-characters'
);
export async function createToken(payload: { userId: string }) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('15m') // 15 分で期限切れ
.sign(secret);
}
export async function verifyToken(token: string) {
try {
const { payload } = await jwtVerify(token, secret);
return payload;
} catch {
return null;
}
}
15m の有効期限に注目してください。7 日、30 日に設定する人も多いですが、トークンが漏れたときの被害が大きくなります。短期 Access Token + 長期 Refresh Token が正攻法です。
ステップ 2:トークンの保存——localStorage は使わない
ここが重要です。多くのチュートリアルが localStorage への保存を教えますが、XSS が来れば一発で盗まれます。
正解は HttpOnly Cookie です。
ログイン API でトークンを返すとき:
// app/api/login/route.ts
import { NextResponse } from 'next/server';
import { createToken } from '@/lib/auth';
export async function POST(request: Request) {
// ユーザー名・パスワード検証...
const token = await createToken({ userId: user.id });
const response = NextResponse.json({ success: true });
response.cookies.set('token', token, {
httpOnly: true, // JS から読めない(XSS 対策)
secure: true, // HTTPS のみ
sameSite: 'lax', // CSRF 対策
maxAge: 900, // 15 分(トークン有効期限と一致)
});
return response;
}
HttpOnly が鍵です。JavaScript からは一切読めないため、XSS でも Cookie を盗めません。
ステップ 3:ミドルウェアで API を保護
トークンができたら、ログイン必須の API をどう守るか。Next.js の Middleware を使います。
middleware.ts を作成:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyToken } from './lib/auth';
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const payload = await verifyToken(token);
if (!payload) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 }
);
}
// 検証成功——ユーザー情報を API に渡す
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', payload.userId as string);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
export const config = {
matcher: '/api/protected/:path*',
};
これで /api/protected/* 以下は自動的に保護されます。API 側でユーザー情報を取得:
// app/api/protected/profile/route.ts
import { headers } from 'next/headers';
export async function GET() {
const headersList = await headers();
const userId = headersList.get('x-user-id');
// DB からユーザー情報を取得...
}
ステップ 4:トークンリフレッシュ
15 分で切れると、ユーザーは頻繁にログインし直す必要がある?ここで Refresh Token を導入します。
Access Token は短期(15 分)、Refresh Token は長期(30 日)。Access Token が期限切れになったら Refresh Token で新しいものを取得し、再ログインは不要です。
実装はやや複雑ですが、考え方はこうです。next-auth など既存ソリューションにはこの仕組みが組み込まれています。
NextAuth.js で素早く統合
正直、上記を全部自分で書くと原理は分かりますが、工数は大きいです。素早く始めたいなら NextAuth.js(現在は Auth.js)をそのまま使いましょう。
このライブラリの強みは、セキュリティのデフォルト設定が揃っていることです。
- CSRF 保護を自動処理
- Session の署名と暗号化
- JWT と DB Session の両対応
- Google、GitHub などの OAuth がすぐ使える
インストール:
npm install next-auth
app/api/auth/[...nextauth]/route.ts を作成:
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
const handler = NextAuth({
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
// ユーザー名・パスワード検証...
if (user) {
return { id: user.id, email: user.email };
}
return null;
}
})
],
session: {
strategy: 'jwt', // Serverless 向けに JWT
maxAge: 30 * 24 * 60 * 60, // 30 日
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.userId = user.id;
}
return token;
},
async session({ session, token }) {
session.userId = token.userId;
return session;
}
}
});
export { handler as GET, handler as POST };
API でログイン状態を確認:
import { getServerSession } from 'next-auth';
export async function GET() {
const session = await getServerSession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// ログイン済み——処理を続行...
}
これだけです。NextAuth がトークン管理と Session 更新を肩代わりしてくれます。
CORS 設定の詳細
CORS の本質とよくある問題
CORS(Cross-Origin Resource Sharing)は多くの開発者を悩ませます。開発環境では問題ないのに、デプロイするとエラーが出る——
Access to fetch at 'https://api.example.com' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present.
要するに、ブラウザには安全ポリシーがあります。サイト A のページからサイト B のリソースに自由にはアクセスできません。app.com のページから api.com の API を呼ぶとき、ブラウザは先に api.com に「app.com からのリクエストを許可しますか?」と確認します。API が明示的に「許可する」と返さないと、リクエストは通りません。
開発環境でエラーが出ない理由
Next.js 開発時はフロントエンドと API が同じ localhost:3000 にあり、同源のため CORS は発生しません。デプロイ後、フロントが Vercel、API が別サーバーになるとクロスオリジンになります。
Preflight リクエストとは
POST でカスタムヘッダー(Authorization など)を付けると、ブラウザは先に OPTIONS で確認します。これが Preflight です。API が OPTIONS を処理していないと 404 になり、CORS が失敗します。
私も POST だけ書いて OPTIONS を忘れ、半日 CORS エラーと格闘したことがあります。
Next.js で CORS を設定する 3 つの方法
方法 1:next.config.js でグローバル設定
すべての API で同じオリジンを許可する場合に向いています。
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: 'https://app.example.com' },
{ key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
],
},
];
},
};
メリットは一度の設定で全体に効くこと。デメリットは API ごとの細かい制御が難しいことです。
方法 2:Middleware で設定
動的判定や一括処理が必要な場合に向いています。
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Preflight を処理
if (request.method === 'OPTIONS') {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': 'https://app.example.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
// 通常リクエストに CORS ヘッダーを追加
const response = NextResponse.next();
response.headers.set('Access-Control-Allow-Origin', 'https://app.example.com');
return response;
}
export const config = {
matcher: '/api/:path*',
};
request オブジェクトにアクセスできるため、送信元に応じて許可可否を動的に決められます。
方法 3:API Route 内で設定
特定 API だけ特殊な要件がある場合に使います。
// app/api/public/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const data = { message: 'Hello' };
return NextResponse.json(data, {
headers: {
'Access-Control-Allow-Origin': '*', // 公開 API——全オリジン許可
},
});
}
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
}
各 API で OPTIONS 処理が必要です。忘れると Preflight が通りません。
CORS セキュリティのベストプラクティス
1. ワイルドカードを乱用しない
よく見かける書き方:
'Access-Control-Allow-Origin': '*'
これは「どのサイトからでも API を叩いていい」という意味です。公開データなら問題ありませんが、ユーザー情報や敏感な操作 API では危険です。
正しくは、許可ドメインを明示します。
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
const origin = request.headers.get('origin');
if (origin && allowedOrigins.includes(origin)) {
response.headers.set('Access-Control-Allow-Origin', origin);
}
2. クレデンシャル送信に注意
API が Cookie(Session 認証など)を読む必要がある場合、フロントエンドはこう書きます。
fetch('https://api.example.com', {
credentials: 'include',
});
バックエンド側も合わせる必要があります。
response.headers.set('Access-Control-Allow-Credentials', 'true');
ただし、Access-Control-Allow-Origin に * は使えません。ブラウザがこの組み合わせを拒否するため、具体的なドメインを指定してください。
3. Preflight リクエストを処理する
Authorization ヘッダーや Content-Type: application/json を含むリクエストは、ほぼ Preflight が発生します。API は OPTIONS メソッドに応答する必要があります。
共通関数を用意すると楽です。
export function corsHeaders(origin?: string) {
return {
'Access-Control-Allow-Origin': origin || 'https://app.example.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400', // Preflight 結果を 24 時間キャッシュ
};
}
各 API で再利用できます。
API レート制限
なぜレート制限が必要か
冒頭の話に戻りましょう。API が 1,800 万回叩かれたとき、IP あたり毎分 100 回に制限していれば、被害は数十ドル程度で済んだはずです。7,800 ドル請求は避けられたでしょう。
レート制限(Rate Limiting)は、一定時間内にユーザーが API を何回呼べるかを制限する仕組みです。シンプルですが、効果は大きいです。
DDoS 対策。大量リクエストでサーバーを落とそうとしても、閾値を超えた分は拒否——サーバーは安定します。
ブルートフォース対策。ログイン API に制限がなければ、1 秒 10,000 パスワードを試せます。IP あたり毎分 5 回に制限すれば、突破難度は指数関数的に上がります。
リソース保護。DB やサードパーティ API にはコストがかかります。制限により、一人のユーザーがリソースを使い切るのを防ぎ、全員に公平なサービスを提供できます。
レート制限ソリューションの比較
Next.js での主流な選択肢は次のとおりです。
方案 1:@upstash/ratelimit + Vercel KV
私が最もよく使う構成です。Upstash は Serverless Redis で、Vercel 公式パートナー。統合も簡単です。
メリット:
- Serverless 向き——Redis サーバーを自前管理不要
- 固定ウィンドウ、スライディングウィンドウ、トークンバケットなど複数アルゴリズム
- 無料枠で個人プロジェクトは十分
デメリット:
- 大流量では有料
- サードパーティ依存
方案 2:自前ホスト Redis
既に Redis がある、またはサードパーティを避けたい場合。
メリット:
- 完全制御、追加コストなし
- 複雑なロジックも自由に実装
デメリット:
- Redis サーバーの運用が必要
- Serverless 環境では設定が複雑
方案 3:メモリ内レート制限
Redis を入れたくない場合の簡易版。
メリット:
- 依存ゼロ、数行で実装
- 開発環境や小規模プロジェクト向け
デメリット:
- Serverless ではインスタンスごとにメモリが分離され、制限が効かない
- サーバー再起動でカウントがリセット
私のおすすめ:個人プロジェクト・Serverless なら Upstash。エンタープライズで自前サーバーなら Redis。デモやローカル開発ならメモリで妥協。
実践コード例
Upstash を例に、手順を追います。
ステップ 1:インストールと設定
npm install @upstash/ratelimit @upstash/redis
Upstash で Redis データベースを作成し、UPSTASH_REDIS_REST_URL と UPSTASH_REDIS_REST_TOKEN を .env に設定:
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token
ステップ 2:レートリミッターを作成
lib/rate-limit.ts:
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
// Redis クライアント
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
// スライディングウィンドウ:10 秒間に最大 10 回
export const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '10 s'),
analytics: true,
});
slidingWindow(10, '10 s') は 10 秒間に 10 回まで。固定ウィンドウより滑らかで、境界での突発的なスパイクを防げます。
ステップ 3:API で使用
// app/api/protected/route.ts
import { NextResponse } from 'next/server';
import { ratelimit } from '@/lib/rate-limit';
export async function GET(request: Request) {
// ユーザー IP を取得
const ip = request.headers.get('x-forwarded-for') || 'unknown';
// レート制限チェック
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{
error: 'Too many requests',
limit,
remaining,
reset: new Date(reset),
},
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
},
}
);
}
// 制限通過——通常処理
return NextResponse.json({ data: 'Success' });
}
ここでは IP を識別子に使っています。ログイン済みなら userId も使えます。
const identifier = session?.userId || ip;
const { success } = await ratelimit.limit(identifier);
ログインユーザーはユーザー単位、未ログインは IP 単位——より精密な制御が可能です。
ステップ 4:Middleware で全体制限
各 API に書くのが面倒なら、Middleware で一括処理:
// middleware.ts
import { ratelimit } from '@/lib/rate-limit';
export async function middleware(request: NextRequest) {
const ip = request.ip || 'unknown';
const { success } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*',
};
すべての API が自動的に保護されます。
入力検証と防御
入力検証が第一の防衛線である理由
「ユーザー入力を決して信頼するな」——セキュリティの鉄則です。
フロントエンドのフォーム検証?開発者ツールで簡単にバイパスできます。本当の防御はサーバー側にあります。
SQL インジェクション。入力欄に '; DROP TABLE users; -- と書かれ、SQL を直接連結すれば DB は壊れます。ORM が主流でも、生 SQL を使う場面は残っています。
XSS 攻撃。<script>alert('hacked')</script> が DB に保存され、他ユーザーがページを開くとスクリプトが実行され、Cookie が盗まれます。React は自動エスケープしますが、dangerouslySetInnerHTML を使えば同様に危険です。
DoS 攻撃。10 MB の JSON を送れば Serverless 関数のメモリが溢れます。超長文字列で正規表現が暴走することもあります。
入力検証で大半の初歩的攻撃を防げます。検証なしでは、他の対策も穴だらけです。
Zod による型安全な検証
Zod は TypeScript 製の検証ライブラリで、型システムと相性抜群です。
インストール:
npm install zod
基本用法
スキーマを定義:
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
age: z.number().int().min(18).max(120),
});
API で検証:
// app/api/register/route.ts
import { NextResponse } from 'next/server';
import { userSchema } from '@/lib/schemas';
export async function POST(request: Request) {
const body = await request.json();
// データ検証
const result = userSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{
error: 'Validation failed',
details: result.error.format(),
},
{ status: 400 }
);
}
// 検証成功——型安全なデータを取得
const { email, password, age } = result.data;
// 処理を続行...
}
safeParse を使うこと。例外を投げずに結果を返します。parse は例外を投げるため try-catch が必要です。
手書き検証より良い理由
手書き:
if (!body.email || typeof body.email !== 'string') {
return error;
}
if (!body.email.includes('@')) {
return error;
}
// 延々と続く...
Zod:
z.string().email()
一行で済み、型も自動推論されます。
完全な入力検証スキーム
検証は型チェックだけではありません。ビジネスロジックと境界条件も考慮します。
1. リクエストボディ(body)の検証
const postSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().max(10000), // 長さ制限(巨大入力対策)
tags: z.array(z.string()).max(10), // 配列長制限
publishedAt: z.string().datetime().optional(),
});
2. クエリパラメータ(query)の検証
// app/api/posts/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const querySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sort: z.enum(['asc', 'desc']).default('desc'),
});
const params = querySchema.parse({
page: searchParams.get('page'),
limit: searchParams.get('limit'),
sort: searchParams.get('sort'),
});
// params.page は必ず数値——型安全
}
z.coerce.number() は文字列を自動で数値に変換してくれます。
3. カスタム検証ルール
const passwordSchema = z.string()
.min(8)
.refine((val) => /[A-Z]/.test(val), 'Must contain uppercase')
.refine((val) => /[a-z]/.test(val), 'Must contain lowercase')
.refine((val) => /[0-9]/.test(val), 'Must contain number');
非同期検証も可能:
const emailSchema = z.string().email().refine(
async (email) => {
const exists = await checkEmailExists(email);
return !exists;
},
'Email already taken'
);
4. エラーハンドリング
Zod のエラーは読みやすいですが、カスタマイズもできます。
if (!result.success) {
const errors = result.error.errors.map(err => ({
field: err.path.join('.'),
message: err.message,
}));
return NextResponse.json({ errors }, { status: 400 });
}
構造化されたエラーを返せば、フロントエンドでの表示も楽です。
その他のセキュリティ対策
検証以外にも、重要な対策があります。
1. CSRF 保護
Next.js の Server Actions には CSRF 保護が組み込まれています。リクエストの Origin と Host を比較し、不一致なら拒否します。
API Routes は自分で実装が必要です。NextAuth を使えば自動処理されます。手動なら CSRF トークンを使います。
// トークンを Cookie に置き、フロントがリクエストに付与、サーバーで照合
2. Content Security Policy(CSP)
next.config.js で CSP を設定し、ページが読み込めるリソースを制限:
{
headers: [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self'; style-src 'self';",
},
],
}
XSS 脆弱性があっても、悪意あるスクリプトの読み込みを防げます。
3. 環境変数の安全
Next.js の環境変数には 2 種類あります。
NEXT_PUBLIC_*で始まるものはフロントエンドに公開される- プレフィックスなしはサーバー側のみ
秘密鍵を NEXT_PUBLIC_ に絶対置かない。NEXT_PUBLIC_API_KEY として API キーを公開してしまった事例があります。
4. SQL インジェクション対策
Prisma、Drizzle などの ORM はパラメータ化クエリを自動で行い、基本的に安全です。
生 SQL を書く場合はパラメータ化を使います。
// ❌ 危険
db.query(`SELECT * FROM users WHERE id = ${userId}`);
// ✅ 安全
db.query('SELECT * FROM users WHERE id = ?', [userId]);
5. 依存関係の定期更新
脆弱性は依存ライブラリからも出ます。定期的に実行:
npm audit
npm update
今年 2 月の React 脆弱性も、最新版に更新すれば修正済みです。面倒でも、更新しないほうがもっと面倒になります。
完全セキュリティチェックリスト
ここまでの内容を、プロジェクト診断用のチェックリストにまとめました。
認証セキュリティ
- ✅ トークンは HttpOnly Cookie に保存(localStorage は使わない)
- ✅ Access Token の有効期限 ≤ 30 分
- ✅ Refresh Token 機構を実装
- ✅ JWT シークレットは 32 文字以上、環境変数に保存
- ✅ Cookie に
secureとsameSiteを設定 - ✅ Middleware で敏感な API を保護
CORS 設定
- ✅ 敏感な API に
Access-Control-Allow-Origin: *を使わない - ✅ 許可ドメインを明示的に指定
- ✅ OPTIONS Preflight を正しく処理
- ✅ クレデンシャル送信時は
Access-Control-Allow-Credentials: true - ✅ 本番環境で CORS 設定が効いているか検証
レート制限
- ✅ ログイン・登録・パスワードリセットなど重要エンドポイントに厳格な制限
- ✅ 認証済み・未認証ユーザーで制限を分ける
- ✅ 429 ステータスと
Retry-Afterヘッダーを返す - ✅ Redis や Upstash など永続ストアを使用(Serverless でメモリ制限は無効)
- ✅ 制限発動状況を監視し、閾値を調整
入力検証
- ✅ すべてのユーザー入力をサーバー側で検証
- ✅ Zod などで型安全に検証
- ✅ 文字列・配列・オブジェクトの最大長を制限
- ✅ メール、URL、日付などの形式を検証
- ✅ 分かりやすい検証エラーを返す
定期監査
- ✅ 月 1 回以上
npm auditを実行し、高危険度を修正 - ✅ Next.js と React を最新安定版に更新
- ✅ Next.js セキュリティ公告を購読
- ✅ 環境変数を監査し、秘密鍵がフロントに漏れていないか確認
- ✅ Code Review で認証・権限ロジックを重点チェック
このチェックリストを印刷して机に貼り、新規プロジェクト開始前とデプロイ前に必ず確認しましょう。
まとめ
長く書きましたが、核心は一言です。API セキュリティは一度設定すれば終わりではなく、継続的なシステム工程です。
認証、CORS、レート制限、検証——全部設定するのは面倒に感じるかもしれません。でも、データ漏洩、サーバーダウン、高額請求が起きてから対処するほうが、はるかにコストが大きいです。
私の経験では、ゼロからこの一式を設定するのに半日〜 1 日かかります。一度整えれば、以降はテンプレートをコピーしてパラメータを変えるだけ。新プロジェクトなら 10 数分で済みます。何より、夜安心して眠れます。朝起きたら攻撃を受けていた——そんな心配がなくなります。
アクション提案:
- 今すぐ既存プロジェクトを確認し、チェックリストと照合
- 最重要から着手——まず認証とレート制限、あとから他を追加
- セキュリティ情報を購読——Next.js 公式ブログ、GitHub Security Advisories など
- チームで共有——セキュリティは一人の仕事ではない
最後に一つ。2025 年 12 月の CVSS 10.0 の React 脆弱性は影響範囲が非常に大きいです。まだ更新していなければ、今すぐ最新版へ。セキュリティ更新の先延ばしは、本当に危険です。
API セキュリティの道は長いですが、一歩一歩は価値があります。この記事が、あなたのプロジェクトでいくつかの地雷を避け、早めに防御を整える助けになれば幸いです。
FAQ
Next.js API では JWT と Session、どちらを使うべきですか?
なぜ JWT トークンを localStorage に保存してはいけないのですか?
開発環境では CORS エラーが出ないのに、本番で出るのはなぜですか?
Serverless 環境でレート制限を実装するには?
API へのブルートフォース攻撃を防ぐには?
8分で読めます · 公開日: 2026年1月5日 · 更新日: 2026年6月8日
Next.js 完全ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Next.js API パフォーマンス最適化完全ガイド:キャッシュ戦略、ストリーミング、エッジコンピューティング実践
Next.js API の応答を 3 秒から 500ms に短縮するには?キャッシュ戦略の選び方、ストリーミングレスポンスの実装、Edge Functions の活用という 3 つの核心技術を、実コード例と性能比較データ付きで解説します。
第 18 / 47 記事
次の記事
Next.js リアルタイムチャット:WebSocket と SSE の正しい使い方
WebSocket、SSE、Long Polling の 3 つのリアルタイム通信方式を深く比較し、Vercel デプロイの実践知見を共有。Socket.io 統合、メッセージ状態管理、パフォーマンス最適化の完全なコード例付き。
第 20 / 47 記事
関連記事
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js Middleware 実践ガイド:パスマッチ、Edge Runtime 制限とよくある落とし穴
コメント
GitHubアカウントでログインしてコメントできます