OpenAINext.jsOct 23, 2023, 2:00 PM

How to Think About Security in Next.js

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

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

How to Think About Security in Next.js

Key Points

  • Prefer a single data access model
  • Return minimized DTOs from the server
  • Use server-only checks and tainting

Summary

This note explains security considerations for Next.js App Router and React Server Components (RSC). It focuses on preventing accidental data exposure by choosing a consistent data-handling model, consolidating sensitive logic on the server, and auditing component interfaces. It calls out built-in protections (server-only modules, import checks) and experimental tainting to reduce leakage risk.

Key Points

  • Pick one data-handling model and stick to it:
    • HTTP APIs: treat Server Components as untrusted like client code; use fetch to internal APIs and apply Zero Trust.
    • Data Access Layer (recommended for new projects): centralize DB access and authorization in a server-side JS library that returns minimized DTOs.
    • Component-level access: only for prototyping; requires strict PR auditing.
  • Minimize client-visible data:
    • Return Data Transfer Objects with only fields that the current user can see.
    • Avoid passing whole user objects, tokens, or credit-card fields to Client Components.
    • Use parameterized queries or an ORM to prevent SQL injection.
  • Enforce server-only code and build-time checks:
    • Use import 'server-only' for modules that must not be bundled to the client (build will fail on improper imports).
    • Keep environment secrets accessed only in the data access layer; NEXT_PUBLIC_* variables are intentionally exposed to the client.
  • Use caching and small helpers on the server:
    • Use cached helpers (e.g., getCurrentUser via react cache) to avoid passing context through many components.
  • Experimental safeguards (Next.js 14):
    • experimental_taintObjectReference and experimental_taintUniqueValue can mark objects/strings as not allowed to be passed to the client, but derived values may still leak — treat tainting as an additional guard, not a replacement for good design.
  • Treat SSR client components like browser code: they must not access privileged data or server-only modules.

Practical audit checklist for PRs

  • Verify a single data model is followed or review exceptions heavily.
  • Check Client Component props and exported functions for broad types (User, token, creditCard, phoneNumber).
  • Ensure server-only modules are not imported in client code.
  • Confirm DB access is parameterized and auth checks live in the data access layer or API endpoints.

Takeaway

Prefer a centralized server-side data access layer that returns minimized DTOs and use server-only module checks and tainting where available. For existing large orgs, HTTP APIs combined with Zero Trust works well; component-level access should be limited to small teams and prototypes.

Full Translation

Translations

A translation section that keeps the flow of the original article.

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