Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Next.js App Router セキュリティ

zaru
November 05, 2024
450

Next.js App Router セキュリティ

zaru

November 05, 2024
Tweet

Transcript

  1. Request Header で Next-Action を送信してる curl で再現するとこんな感じの POST 通信 curl

    'https://nextclicker.nanabit.dev/' \ -H 'Content-Type: text/plain;charset=UTF-8' \ -H 'Cookie: ...' \ -H 'Next-Action: 6eb8416051487420c0347306825a392adf55f29e' \ --data-raw '[1]' 手動でリクエストを試してみる
  2. --data-raw '[9999]' の数値をいじると、 1 クリックで上がる 数値を変更可能なことが分かった curl 'https://nextclicker.nanabit.dev/' \ -H

    'Content-Type: text/plain;charset=UTF-8' \ -H 'Cookie: ...' \ -H 'Next-Action: 6eb8416051487420c0347306825a392adf55f29e' \ --data-raw '[9999]' # 1 クリックで9999 上がる 任意の数値でスコアを挙げられることを発見
  3. "use server"; export async function incrementalScore(power: number) { const user

    = await currentUser(); if (!user) return; await prisma.user.update({ where: { id: user.id }, data: { score: { increment: power } }, }); revalidatePath("/"); } Bad: 引数で上げる数値を受け取り信頼してしまっている Server Component 関数の実装
  4. Server Actions = HTTP エンドポイント 見た目は関数を呼び出しているが HTTP エンドポイント 自動でエンドポイントをたて、 内部で

    fetch している つまり外部に公開されているので引数は信頼できない 引数は必ずパースし、 必要なら認証もする 外部公開 API と捉えて設計と実装をすること こう考える
  5. DevTools の Search で [0-9a-z]{40} で検索する Request Header で Next-Action

    で使う ID が見つかる Server Actions の探し方
  6. curl 'https://nextclicker.nanabit.dev/' \ -H 'Content-Type: text/plain;charset=UTF-8' \ -H 'Next-Action: 8d2238ce9fde355707d6a4b613a12f5d5360427c'

    \ --data-raw '[1]' 0:["$@1",["EBYDKshKtbxTnpEuKdpC4",null]] 1:{"id":1,"name":"zaru","password":"$$2b$10$k6BnP5...","score":280,"level":0} ハッシュ化されたパスワードが返ってきた・ ・ ・! 適当にリクエストしてみる
  7. "use server"; は Server Actions にするディレクティブ 名前から 「サーバ処理をする時に付けるもの」 と勘違い 盲目的に、

    DB を叩く関数すべてにつけてしまった・ ・ ・ "use server"; # 内部でしか使ってないのに、 意図せず外部公開されてしまった関数 export async function fetchRankers() { return prisma.user.findMany({ orderBy: { score: "desc" }, take: 10, }); } 意図せず Server Actions になってしまった例
  8. "use server"; != サーバ処理 上記を理解していても 1 ファイルに複数の関数を export して いると、

    気が付かずに ServerActions になってしまうものが出 てくる可能性 v15 ではID が露出しないように改善された Knip というツールで未 export 関数の検出可能 こう考える
  9. Server Component で取得したユーザ情報をそのまま Client Component に渡している // Ranking はServer Component

    export async function Ranking() { const users = await fetchRankers(); return ( {users.map((user) => ( // RankingItem はClientComponent <RankingItem key={user.id} user={user} /> ))} ); } Server と Client の境界線
  10. 必要な情報だけ props に渡す コンポーネントが Server か Client かで考えると境界線意識 がつらい どんなコンポーネントでも必要な情報だけ扱うようにする

    TaintAPI や Pick 型/Zod パースなどを使う それぞれ効能が少し異なるのでシーンによって使い分ける こう考える
  11. SQL なら原則 SELECT 句は指定した方が良い export async function fetchRankers() { return

    prisma.user.findMany({ // Prisma の例 select: { id: true, name: true, score: true, level: true }, }); } SELECT 句を明示的にする
  12. React の提供する Taint API を使うと使用禁止をマークできる サーバ側でしか使わないようなオブジェクトに対して利用可能 ただし、 実行時エラーなのでエディタ上では分からない export async

    function fetchRankers() { const users = await prisma.user.findMany(); for (const user of users) { // user オブジェクトをClient Component に渡すとエラーになる taintObjectReference("Client Component では使えません", user); } return users; } Taint API を使う
  13. ライブラリでパースし、 必要ないプロパティを落とす const schema = z.object({ id: z.number(), name: z.string(),

    }); // パースすると、 id / name 以外のプロパティは消える const parsed = schema.safeParse(user); Zod でパースする
  14. import 'server-only'; で Client Component に含ませない next-safe-action という Server Actions

    を少し安全にする https://next-safe-action.dev/ 似たようなライブラリは他にもいくつかある おまけ