🚒

これからNext.jsを始める人に注意してほしいセキュリティ事項

2025/01/02に公開

Next.js v14からServer Actionsがstableリリースとなり、開発体験だけでなく、ユーザ体験(ハイドレーションの完了を待機せずフォーム操作ができる、プログロレッシブ•エンハンスメントなど)の向上が見込まれます[1]

今回は、Next.jsのServer Actionsを実装する上で注意しなければならないセキュリティ懸念事項とその対策を提案します。

Server Actionsとは

formからsubmitされた際の処理(DBの更新等)をサーバサイドで非同期で実行できる関数です。ReactのServer ComponentsだけでなくClient Componentsからも呼びだすことができます。つまり、formがsubmitされた際のDB更新などの処理をわざわざエンドポイントを立てずに関数として書くことができます。[2]

Server Actions(またはServer Components)のセキュリティリスクと対策

  1. Server ActionsやServer ComponentsをClinet Componentsから利用する場合、Client Componentsへpropsで渡したデータは全て露出します。

  2. Server Actionsとして定義した関数をexportすると、その関数はAPIとして公開されます。
    この辺りに関しては、ムーザルちゃんねるさんの記事などが参考になります。
    なので、Server Actionsが公開APIとなるということを前提にセキュリティ対策を講じる必要があります。

はじめに

How to Think About Security in Next.jsにあるように、DBのデータなどを扱うレイヤーの選定が重要です。(以下はざっくり書いているため、詳細は公式リンク日本語訳を参照してください。)

  • HTTP APIs
    既存のプロジェクトなどでServer Componentsを導入する場合、REST APIやGraphQLなどをServer Componentsから呼び出す。
  • Data Access Layer
    新しいプロジェクトでは、Next.jsのコードベース内にデータアクセスに関する処理を統合する。ユーザの権限を確認してから、DTO(後述)の構築などを通してセキュリティを確保する。
  • Component Level Data Access
    プロトタイピングなど用。DBクエリを直接サーバコンポーネントに書く。

対策その1: 本当に必要な情報以外クライアントコンポーネントに渡さない

'use server';
import {getUserById} from '@/lib/db'
import {ProfileCard} from '@/component/ProfileCard'

export const fetchUser = async(userId:string)=> {
    const user = getUserById(userId);
    return user;
}

// Profile is Server Components
// ProfileCard is Client Components
export const Profile:React.FC = async(userId:string) =>{
    const user = await fetchUser(userId);
    return (
        <ProfileCard user={user} />
    );
}

上記の場合、DB上のユーザ情報を全てClinet Componentsに渡しています。すなわち、id,name意外にもpasswordなど秘匿情報も全てクライアントに露出します。

  1. zodなどのライブラリでパースする
  2. Taint APIを用いる
  3. DTOを用いる

zodなどのライブラリでパースする

const schema = z.object({
    name: z.string(),
});

const parsedUserProfile = schema.safeParse(user);

Taint APIを用いる

Reactが提供するAPIで秘匿情報などをClient Componentsに渡すことを禁止します。

export const getUserWithTaint = async(userId:string) => {
  const user = await getUserById(userId);
 

  // user を Client Component からアクセス禁止にする
  taintObjectReference(
    "Clinet Compoentsにuserオブジェクトを渡すことは禁止です。", 
    user
  );
  return user;
};

DTOを用いる

DTOの書き方はいろいろありますが、言いたいことはすなわち、dbから取得したuserオブジェクトからクライアントに渡したいプロパティのみ抽出する関数です。以下の例はとても単純ですが、現在ログインしているユーザの権限に応じてそれぞれのプロパティを返すか処理を加えることもできます。

export interface UserDTO {
  id: string;
  name: string;
  email: string;
}

export const getProfileDTO = async(userId:string):UserDTO => {
  const currentUser = await getUserById(userId)
 
  return {
    id: currentUser.id,
    name: currentUser.name,
    email: currentUser.email
  }
}

対策その2: 認証、認可の処理を追加する

認証をどこでやるかはこちらこちらが参考になります。Next.js v15より前はlayout.tsxで認証するのはいけなかったのですがレンダリングの順序が変わったため、実装可能かもしれません。しかし、Next.jsのレンダリングの順序が将来的に変わらないとも言えないので議論の余地がありそうです。

いずれにせよ、server actionsが公開APIとなっているため、認証してロールを判別し、処理の不正な実行を防止する必要があります。

'use server'
import { verifySession } from '@/app/lib'
 
export const deleteDBAction = async () {
  const session = await verifySession()
  const userRole = session?.user?.role
 
  if (userRole !== 'admin') {
    return null
  }
 
}

ここでは簡単にセッションを検証するverifySession関数があると仮定していますが、jose,NextAuth.jsなどのライブラリを用いると便利だと思います。

対策その3: バリデーションをサーバサイドで行う

認証ももちろん大切ですが、zodなどをもちいてServer Actionsに不正なリクエストデータが渡ってきた場合にガードするためにも導入は必要です。
クライアントコンポーネントにバリデーションを導入するケースが従来はあったと思いますが、Next.jsでバックエンドのロジックも含め完結する場合はServer Actionsにもバリデーションを導入するべきです。

対策その4: import 'server-only'; を利用する

クライアントコンポーネントにインポートしようとするとビルドエラーが発生します。
秘匿APIキーなどを利用した関数などを使用しているファイルなどに記載すると良いと思います。

対策その5: CSRF対策

公式ブログでは、Same-Site Cookieがデフォルトで使われるため、ほとんどCSRF攻撃は防げると記載があります。
また、Next.js v14以降、OriginヘッダーとHostヘッダー(またはX-Forwarded-Host)を比較し、一致しない場合はアクションを拒否するようになっていると言及しています。
そのためCSRF対策は、ルートハンドラを定義する時意外は今の所対策がされているようです。

まとめ

今回の記事を書くにあたり、サーバサイドのセキュリティを担保してきた身としては、ある意味昔から行われている当たり前の対策が多い印象に思われましたが、Webアプリケーションを構築するにあたり注意するべきポイントを再認識できたと思います。
React Server ComponentおよびServer Actionsにより、フロントエンドとバックエンドがシームレスに統合され、Webアプリケーション(とくにフロントエンド)のユーザ体験向上がますます期待されます。ただ、バックエンドサーバがになっていた処理がNext.jsのコンポーネント・関数に含まれるようになったため負荷の思い処理やアクセス数が多いアプリケーションに導入するには、Next.jsのデプロイ先のリソースについて考慮しなければならないと考えています。また、RailsやLaravelといったバックエンドフレームワークを使い、リクエストに応じた処理およびJSONを返すサーバサイドアプリケーションを開発する時代から、Next.jsによってバックエンドの責務も担う時代に入っていくことも考えられますが、複雑なビジネスロジックを含む場合などは、Next.jsで完結させるべきかよく考えることが必要だと思います。

参考リンク

脚注
  1. なぜ Server Actions を使うのか ↩︎

  2. Server Actions and Mutations ↩︎

Discussion