🦁

【Next.js】<input type="hidden" /> を使わずに安全にフォームデータを送る方法

に公開3

📝 はじめに

Next.js 14 では、Server Actions や useActionState を使うことで、フォーム送信の新しいスタイルが登場しました。この記事では、よくある「ToDoアプリ」を題材に、input type="hidden" を使わずに安全にサーバーに値を送る方法を紹介します。

🏡 よくある実装:<input type="hidden"/> の問題点

ToDoの編集フォームを作るとき、更新対象のID(todoId)を <input type="hidden"/> で渡す実装をしがちです。

<input type="hidden" name="todoId" value={todo.id} />

一見便利そうに見えますが、これは改ざんされる可能性があるため、セキュアな実装とは言えません。

  • 🚨 ユーザーが todoId を書き換えられる

  • 🚨 他人の ToDo を編集できてしまう可能性も…

✅ 解決策:formData.set() を使って値をプログラムで追加

useActionState を使えば、フォームに todoId を含めることなく、値をプログラムで追加して送信できます。

'use client'

import { useActionState } from 'react'
import { updateTodo } from '@/lib/actions'

export default function EditTodoForm({ todo }) {
  const [state, formAction] = useActionState(
    async (prevState, formData) => {
      // ← ここが重要!クライアント側でフォームに含めず値を追加
      formData.set('todoId', todo.id)
      return await updateTodo(prevState, formData)
    },
    {
      success: false,
      errors: {},
    }
  )

  return (
    <form action={formAction}>
      <input type="text" name="title" defaultValue={todo.title} />
      <button type="submit">更新</button>
    </form>
  )
}

🔐 なぜ安全なのか?

  • todoId はフォームに含まれない

  • hidden input に比べて、HTML上での目視による簡単な改ざんを防ぎやすい

しかし、これだけでは不十分です。絶対に必要なのは、以下のようなサーバー側の認可処理です。

本当に安全にするには

この方法は、ただ「値をフォームに含めない」だけで、JavaScript を書き換えれば、自由に todoId を仕込んだリクエストを送信できます。

それゆえ、サーバー側で「本当にその ToDo を変更してよいユーザーか?」を確認する処理が一番重要です。

🔧 サーバー側の例

'use server'
import { getServerSession } from 'next-auth'
import { db } from '@/lib/db'

export async function updateTodo(prevState: any, formData: FormData) {
  const session = await getServerSession()
  if (!session) {
    throw new Error('Unauthorized')
  }

  const todoId = formData.get('todoId') as string
  const title = formData.get('title') as string

  const todo = await db.todo.findUnique({ where: { id: todoId } })
  if (!todo || todo.userId !== session.user.id) {
    throw new Error('Forbidden')
  }

  await db.todo.update({
    where: { id: todoId },
    data: { title },
  })

  return { success: true, errors: {} }
}

⚠️ 注意点

  • todo の情報はサーバーから取得し props に渡すようにしましょう(例: getTodo())

  • 認証済みユーザーかどうかの確認や、許可のあるIDかの認可処理は server action 側で必ず実装しましょう

✅ まとめ

このように、useActionState によりフォームの見た目をシンプルに保ちながら、hidden フィールドを使わずに値を渡すことができます。

Next.js 14 の useActionState と Server Actions を使えば、input type="hidden" に頼らず、フォームUI上の改ざんリスクを削減する手段として有用です。

しかし、本当の安全性を確保するためには、最終的にサーバー側での認可ロジックが一番重要です。「その ToDo を編集してよいのは本当にこのユーザーなのか」を server action の中でしっかり確認するようにしましょう。

Discussion

Honey32Honey32

失礼します。

まず前提として、 'use client' の付いたファイル内に書かれた処理は、クライアント側の処理です。 useActionState でくるんだからといって、サーバー側の処理にはなってくれません。(Client Action になります)ここに誤解があるように見受けられます。

なので、正しい例として挙げられた方法であっても、「自由に todoId を指定してしまえる」問題の解決にはなっていません。Server Action は、POST のエンドポイントとして露出されているからです。

たぶんこんな感じで破れるはずです

ブラウザで DevTools を開いて、JS ファイルの中から formData.set('todoId', todo.id) を見つけ出して、todo.id を任意の id に書き換えるだけで、任意のタスクを対象とした更新リクエストを送信できます。

「他人の作った todo を更新できないようにする」ためには、クライアント側ではなく、サーバー側で(つまり updateTodo の中で)ガードする必要があります。

僕の詳しくない分野なので詳細は言えませんが、「リクエストを送信したユーザーの認証情報を取り出して、その todoId の todo を編集する権限があるかをチェックする。権限がない場合は失敗させる」処理が必要になるのではないかと思います。

takumatakuma

コメントありがとうございます!ご指摘いただいた点、まさにその通りです。

記事では formData.set('todoId', todo.id) を使うことで hidden input を使わずに値を渡す方法を紹介しましたが、これはあくまで「UI上での改ざんリスクを軽減する手段の一つ」に過ぎません。

ご指摘のように、useActionState の内部処理や formData.set() はクライアント側で実行されるため、DevTools などを使えば容易に書き換え可能です。さらに Server Action が POST エンドポイントとして露出している以上、任意のリクエストを外部から送信できてしまうという点も非常に重要なポイントです。

本当に重要なのはおっしゃるとおり、「サーバー側での認可チェック(authorization)」です。
実際に記事の後半でも、以下のような処理を推奨・実装例として紹介させていただきました:

  • 認証済みユーザーの取得(getServerSession()など)
  • 送信された todoId に対する ToDo データの取得
  • その ToDo がログイン中のユーザーに属しているかの検証
  • 一致しない場合はエラーを返す

コメントをきっかけに、この記事で扱っている手法が「改ざんリスクの軽減にはなるが、完全なセキュリティ対策ではない」という点を、さらに明確に伝える必要があると再認識しました。

ご丁寧かつ的確なご指摘、本当にありがとうございました!

Honey32Honey32

修正されたのか、もともとそうだったのを見逃したのか分からなくなっちゃいましたが…とにかくいいですね!

「アクション = サーバーアクション」ではなく、クライアント側のアクションもある、ということは、以下の記事だとわかりやすいかと思います。

https://ja.react.dev/blog/2024/12/05/react-19#actions