OpenAINext.js2023/10/23 14:00

How to Think About Security in Next.js

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

openaijamodel: gpt-5-mini-2025-08-07

Next.js(App Router)におけるセキュリティの考え方

Key Points

  • データアクセス層を推奨
  • サーバー専用コードで漏洩防止
  • 最小限のプロップを渡す

Summary

React Server Components(RSC)を含むApp Routerでは、サーバーとクライアントの境界が曖昧になるため、誤って機密データをクライアントへ露出するリスクに注意する必要があります。この記事は、採用するデータ扱いモデルの選び方(HTTP API / Data Access Layer / Component-level)、組織ごとの推奨、組み合わせを避ける理由、ならびに監査時の実務的チェックポイントをまとめたものです。

Key Points

  • モデルを一つに統一する:HTTP API(既存大規模プロジェクト向け)、Data Access Layer(新規プロジェクト推奨)、Component Level(プロトタイプのみ)。例外は監査で目立つ。
  • Data Access Layer推奨の利点:一箇所にデータアクセスを集約し認可チェックを統一、DTOを返してクライアントに安全な形で渡す。process.envはデータ層のみで扱う。
  • HTTP API方式:Server Componentsからfetchをゼロトラストで扱い、内部ネットワークを信用しない。既存のバックエンドチームや非JS言語との分離が容易。
  • Clientへ渡すデータは最小化:Client Componentのpropsは必要最小限に限定し、Userオブジェクトやトークン等の生データを渡さない。
  • サーバー専用コード:import 'server-only' を使ってサーバーのみ実行を保証。ビルドでクライアント流出を検出する。
  • 実験的保護:Next.js 14のtaint API(experimental_taintObjectReference / experimental_taintUniqueValue)でオブジェクトやユニーク値の直接渡しをブロック(ただし派生値は防げない)。
  • その他の実務対策:パラメタライズドクエリでSQL注入を防ぐ、キャッシュ経由で現在ユーザー情報を共有し過剰な引き渡しを避ける、'use client'ファイルの公開インターフェースをレビューする。

Audit Checklist(短縮)

  • プロジェクトで採用するデータモデルは一貫しているか?
  • サーバー専用モジュールがクライアントにインポートされていないか?
  • Client Componentのpropsは最小限か(User全体やトークンを渡していないか)?
  • DBクエリはパラメタライズドか?
  • 環境変数はNEXT_PUBLICプレフィックスのルールに従っているか?
  • Data Access Layerがある場合、すべてのAPIが現在ユーザーで認可チェックしているか?

エンジニア向けに実践的な第一歩は、プロジェクト方針を決めて(推奨:Data Access Layer)、クライアントに渡すDTOとサーバー専用モジュールの境界を明確にすることです。

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

openaijamodel: gpt-5-mini-2025-08-07

Next.js におけるセキュリティの考え方

概要

React Server Components (RSC) と App Router は、従来の手法に伴う冗長性や潜在的リスクの多くを排除する新しいパラダイムです。新しい仕組みであるがゆえに、開発者やセキュリティチームは既存のセキュリティプロトコルをこのモデルに合わせるのに苦労するかもしれません。本書は注意すべき領域、組み込みの保護機能、およびアプリケーション監査のためのガイドを示し、特に偶発的なデータ露出のリスクに焦点を当てます。

データ処理モデルの選択

React Server Components はサーバーとクライアントの境界をあいまいにします。どこで情報が処理され、どこで利用可能になるかを理解するために、データ処理方針の決定が重要です。まずは、プロジェクトに適したデータ処理アプローチを選びます。推奨される選択肢は次の通りです。

  • HTTP APIs(既存の大規模プロジェクト/組織に推奨)
  • Data Access Layer(新しいプロジェクトに推奨)
  • Component Level Data Access(プロトタイピングや学習用に推奨)

1つのアプローチに固執し、混在させすぎないことを推奨します。これによりコードベースで作業する開発者やセキュリティ監査担当者にとって期待値が明確になり、例外が疑わしいものとして目立ちます。

HTTP APIs

既存プロジェクトで Server Components を採用する場合、推奨されるアプローチは Server Components を SSR やクライアントと同様にデフォルトで安全でない/信頼できないものとして扱うことです。内部ネットワークや信頼ゾーンを前提にせず、Zero Trust の概念を適用できます。代わりに、Server Components からはクライアント上で実行しているかのように fetch() を使って REST や GraphQL といったカスタム API エンドポイントを呼び出します。クッキーを適切に渡すことも忘れないでください。

既存で getStaticProps / getServerSideProps がデータベースに接続している場合は、モデルを統合してこれらも API エンドポイントに移すことを検討するとよいでしょう。内部ネットワークからの fetch を安全と仮定しているアクセス制御に注意してください。

このアプローチにより、既存のバックエンドチームが従来のセキュリティ慣行を適用でき、JavaScript 以外の言語を用いるチームとも相性が良いです。Server Components の利点(クライアントへ送るコードが少なくなることなど)も活かせ、データウォーターフォールは低レイテンシで実行できます。

Data Access Layer

新規プロジェクトに対する推奨アプローチは、JavaScript コードベース内に独立した Data Access Layer を作成し、すべてのデータアクセスをそこに集約することです。この方法は一貫したデータアクセスを保証し、認可バグが発生する可能性を減らします。単一ライブラリに統合することで保守もしやすく、チームの一体感(単一言語)にも寄与します。

また、ランタイムオーバーヘッドが小さく高いパフォーマンスが得られ、リクエストの異なる部分間でインメモリキャッシュを共有できる利点もあります。内部の JavaScript ライブラリは呼び出し元に渡す前にカスタムのデータアクセスチェックを提供します。HTTP エンドポイントに似ていますが、同じメモリモデル内で動作します。

  • すべての API は現在のユーザーを受け取り、そのユーザーがそのデータを閲覧できるかを確認してから返すべきです。
  • 原則として、Server Component の関数本体は、リクエストを発行した現在のユーザーが許可されているデータのみを見るべきです。

この段階以降は通常の API 実装におけるセキュリティ慣行が適用されます。例として、Data Access の設計例を示します。

data/auth.tsx
import { cache } from 'react' ;
import { cookies } from 'next/headers' ;
// Cached helper methods makes it easy to get the same value in many places
// without manually passing it around. This discourages passing it from Server
// Component to Server Component which minimizes risk of passing it to a Client
// Component.
export const getCurrentUser = cache ( async () => {
  const token = cookies().get('AUTH_TOKEN');
  const decodedToken = await decryptAndValidate(token);
  // Don't include secret tokens or private information as public fields.
  // Use classes to avoid accidentally passing the whole object to the client.
  return new User(decodedToken.id);
});

data/user-dto.tsx
import 'server-only';
import { getCurrentUser } from './auth';
function canSeeUsername(viewer: User) {
  // Public info for now, but can change
  return true;
}
function canSeePhoneNumber(viewer: User, team: string) {
  // Privacy rules
  return viewer.isAdmin || team === viewer.team;
}
export async function getProfileDTO(slug: string) {
  // Don't pass values, read back cached values, also solves context and easier to make it lazy
  // use a database API that supports safe templating of queries
  const [rows] = await sql`SELECT * FROM user WHERE slug = ${slug}`;
  const userData = rows[0];
  const currentUser = await getCurrentUser();
  // only return the data relevant for this query and not everything
  // <https://www.w3.org/2001/tag/doc/APIMinimization>
  return {
    username: canSeeUsername(currentUser) ? userData.username : null,
    phonenumber: canSeePhoneNumber(currentUser, userData.team) ? userData.phonenumber : null,
  };
}

これらのメソッドはクライアントにそのまま渡して安全なオブジェクト(Data Transfer Objects、DTO)を公開するべきです。実際には Server Components のみで消費されることもあります。これによりレイヤリングが作られ、セキュリティ監査は主に Data Access Layer に集中でき、UI は迅速に反復できます。対象範囲が小さく、カバーすべきコードが少ないためセキュリティ問題を見つけやすくなります。

import {getProfile} from '../../data/user'
export async function Page({ params: { slug } }) {
  // This page can now safely pass around this profile knowing
  // that it shouldn't contain anything sensitive.
  const profile = await getProfile(slug);
  ...
}

秘密鍵は環境変数に保存できますが、このアプローチでは process.env にアクセスするのは Data Access Layer のみとするべきです。

Component Level Data Access

別のアプローチは、データベースクエリを直接 Server Components に書くことです。これは迅速な反復やプロトタイピングにのみ適しています(小さなチームでリスクと監視方法を全員が理解している場合など)。この方法を採る場合は、"use client" ファイルを慎重に監査してください。

PR のレビュー時には、エクスポートされた関数すべてを確認し、型シグネチャが User のような過度に広いオブジェクトを受け取っていないか、tokencreditCard のような props を含んでいないかを注意深く見ます。電話番号などのプライバシーに敏感なフィールドも特別な注意が必要です。Client Component はその仕事を行うために必要な最小限のデータ以上を受け取ってはいけません。

import Profile from './components/profile.tsx';
export async function Page({ params: { slug } }) {
  const [rows] = await sql`SELECT * FROM user WHERE slug = ${slug}`;
  const userData = rows[0];
  // EXPOSED: This exposes all the fields in userData to the client because
  // we are passing the data from the Server Component to the Client.
  // This is similar to returning `userData` in `getServerSideProps`
  return <Profile user={userData} />;
}

'use client';
// BAD: This is a bad props interface because it accepts way more data than the
// Client Component needs and it encourages server components to pass all that
// data down. A better solution would be to accept a limited object with just
// the fields necessary for rendering the profile.
export default async function Profile({ user }: { user: User }) {
  return (
    <div>
      <h1>{user.name}</h1>
      ...
    </div>
  );
}

常にパラメタライズドクエリ、あるいはそれを提供する DB ライブラリを使用して SQL インジェクション攻撃を回避してください。

Server Only

サーバーでのみ実行されるべきコードは次のようにマークできます。

import 'server-only';

これにより Client Component がこのモジュールを import しようとするとビルドエラーになります。これを利用して、機密性の高いコードや内部ビジネスロジックが誤ってクライアントへ漏れるのを防げます。

主なデータ転送手段は React Server Components のプロトコルで、Client Components に props を渡すときに自動で行われます。このシリアライズは JSON のスーパーセットをサポートします。カスタムクラスの転送はサポートされずエラーになります。したがって、サーバーからクライアントへ誤って大きなオブジェクトが露出するのを避けるトリックとして、データアクセスレコードにクラスを使うことが推奨されます。

次の Next.js 14 リリースでは、experimental React Taint APIs を next.config.js の taint フラグで試すこともできます。

next.config.js
module.exports = {
  experimental: {
    taint: true,
  },
};

これにより、クライアントへそのまま渡すべきでないオブジェクトをマークできます。例:

app/data.ts
import { experimental_taintObjectReference } from 'react';
export async function getUserData(id) {
  const data = ...;
  experimental_taintObjectReference('Do not pass user data to the client', data);
  return data;
}

app/page.tsx
import { getUserData } from './data';
export async function Page({ searchParams }) {
  const userData = getUserData(searchParams.id);
  return <ClientComponent user={userData} />; // error
}

ただし、この仕組みはオブジェクトからフィールドを取り出して渡すことを防ぐものではありません:

app/page.tsx
export async function Page({ searchParams }) {
  const { name, phone } = getUserData(searchParams.id);
  // Intentionally exposing personal data
  return <ClientComponent name={name} phoneNumber={phone} />;
}

トークンのようなユニークな文字列については experimental_taintUniqueValue を使って生の値をブロックすることもできます。

app/data.ts
import { experimental_taintObjectReference, experimental_taintUniqueValue } from 'react';
export async function getUserData(id) {
  const data = ...;
  experimental_taintObjectReference('Do not pass user data to the client', data);
  experimental_taintUniqueValue('Do not pass tokens to the client', data, data.token);
  return data;
}

しかし、これでも導出された値をブロックすることはできません。そもそもデータが Server Components に入らないようにする(Data Access Layer を使う)ほうが優れています。Taint チェックはミスに対する追加の保護層を提供します。関数やクラスは既に Client Components に渡されることがブロックされている点にも留意してください。複数の層を設けることで、何かがすり抜けるリスクを最小化します。

デフォルトでは環境変数はサーバーでのみ利用可能です。慣習として、Next.js は NEXT_PUBLIC_ プレフィックスの付いた環境変数をクライアントへ公開します。これによりクライアントで利用すべき明示的な設定のみを公開できます。

SSR と RSC

初回ロード時、Next.js は HTML を生成するために Server Components と Client Components の両方をサーバーで実行します。Server Components (RSC) は Client Components とは別のモジュールシステムで実行され、両者の間で情報が誤って露出するのを防ぎます。SSR を通じてレンダリングされる Client Components はブラウザクライアントと同じセキュリティポリシーで扱うべきであり、特権データやプライベート API へアクセスしてはなりません。グローバルオブジェクトにデータを隠すようなハックでこの保護を回避するのは強く推奨されません。本質的には、このコードはサーバーでもクライアントでも同様に実行できるべきです。

セキュリティをデフォルトで堅牢にするという方針に沿って、Client Component から server-only モジュールをインポートするとビルドが失敗します。

読むべき点

In Next.js App Router, reading data from a database or API is implemented by rendering Server Component pages. The input to pages a