概要
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' ;
export const getCurrentUser = cache ( async () => {
const token = cookies().get('AUTH_TOKEN');
const decodedToken = await decryptAndValidate(token);
return new User(decodedToken.id);
});
data/user-dto.tsx
import 'server-only';
import { getCurrentUser } from './auth';
function canSeeUsername(viewer: User) {
return true;
}
function canSeePhoneNumber(viewer: User, team: string) {
return viewer.isAdmin || team === viewer.team;
}
export async function getProfileDTO(slug: string) {
const [rows] = await sql`SELECT * FROM user WHERE slug = ${slug}`;
const userData = rows[0];
const currentUser = await getCurrentUser();
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 } }) {
const profile = await getProfile(slug);
...
}
秘密鍵は環境変数に保存できますが、このアプローチでは process.env にアクセスするのは Data Access Layer のみとするべきです。
Component Level Data Access
別のアプローチは、データベースクエリを直接 Server Components に書くことです。これは迅速な反復やプロトタイピングにのみ適しています(小さなチームでリスクと監視方法を全員が理解している場合など)。この方法を採る場合は、"use client" ファイルを慎重に監査してください。
PR のレビュー時には、エクスポートされた関数すべてを確認し、型シグネチャが User のような過度に広いオブジェクトを受け取っていないか、token や creditCard のような 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];
return <Profile user={userData} />;
}
'use client';
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} />;
}
ただし、この仕組みはオブジェクトからフィールドを取り出して渡すことを防ぐものではありません:
app/page.tsx
export async function Page({ searchParams }) {
const { name, phone } = getUserData(searchParams.id);
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