Next.jsにおけるセキュリティの考え方
2023年10月23日(月)投稿者:Sebastian Markbåge @ sebmarkbage
App RouterのReact Server Components(RSC)は、従来の手法に関連する冗長性と潜在的なリスクの多くを排除する新しいパラダイムです。この新しさゆえに、開発者とセキュリティチームは、既存のセキュリティプロトコルをこのモデルに合わせることが困難な場合があります。この文書は、注意すべき領域、組み込まれた保護機能を強調し、アプリケーションの監査ガイドを含むことを目的としています。特に偶発的なデータ露出のリスクに焦点を当てています。
データ処理モデルの選択
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エンドポイントのみを呼び出します。すべてのCookieを渡します。
データベースに接続する既存のgetStaticProps/getServerSidePropsがある場合、モデルを統合し、これらをAPIエンドポイントに移動して、1つの方法で物事を行うことをお勧めします。内部ネットワークからのフェッチが安全であると仮定するアクセス制御に注意してください。
このアプローチにより、セキュリティに特化した既存のバックエンドチームが既存のセキュリティプラクティスを適用できる既存の組織構造を維持できます。これらのチームがJavaScript以外の言語を使用している場合、このアプローチでうまく機能します。
Data Access Layer
新規プロジェクトに推奨されるアプローチは、JavaScriptコードベース内に別個のData Access Layerを作成し、すべてのデータアクセスをそこに統合することです。このアプローチにより、一貫したデータアクセスが保証され、認可バグが発生する可能性が減少します。単一のライブラリに統合するため、保守も容易になります。
現在のユーザーを受け入れ、データを返す前にユーザーがこのデータを見ることができるかどうかをチェックする内部JavaScriptライブラリを構築します。原則として、Server Component関数本体は、リクエストを発行している現在のユーザーがアクセスを許可されているデータのみを見るべきです。
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);
});
import 'server-only';
import { getCurrentUser } from './auth';
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)と呼び、クライアントが消費する準備ができていることを明確にします。
Component Level Data Access
別のアプローチは、データベースクエリを直接Server Componentsに配置することです。このアプローチは、迅速な反復とプロトタイピングにのみ適しています。
このアプローチでは、「use client」ファイルを慎重に監査する必要があります。監査とPRレビューの際は、すべてのエクスポートされた関数と、型シグネチャがUserのような過度に広範なオブジェクトを受け入れるか、tokenやcreditCardのようなプロパティを含むかどうかを確認してください。
Server Only
サーバーでのみ実行されるべきコードは、次のようにマークできます:
import 'server-only';
これにより、Client Componentがこのモジュールをインポートしようとするとビルドエラーが発生します。
Next.js 14の実験的機能
次期Next.js 14リリースでは、next.config.jsでtaintフラグを有効にすることで、実験的なReact Taint APIを試すことができます:
module.exports = {
experimental: {
taint: true,
},
};
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;
}
トークンなどの一意の文字列の場合、taintUniqueValueを使用して生の値もブロックできます。
環境変数
デフォルトでは、環境変数はサーバーでのみ利用可能です。慣例により、Next.jsはNEXT_PUBLIC_で始まる環境変数もクライアントに公開します。これにより、クライアントで利用可能であるべき特定の明示的な設定を公開できます。
SSR vs RSC
初期ロードでは、Next.jsはHTMLを生成するためにServer ComponentsとClient Componentsの両方をサーバーで実行します。Server Components(RSC)は、2つのモジュール間で情報が誤って露出することを避けるため、Client Componentsとは別のモジュールシステムで実行されます。
Server-side Rendering(SSR)を通じてレンダリングされるClient Componentsは、ブラウザクライアントと同じセキュリティポリシーとして考慮されるべきです。特権データやプライベートAPIにアクセスすべきではありません。