Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Next.js App Router セキュリティ

Avatar for zaru zaru
November 05, 2024
8.2k

Next.js App Router セキュリティ

Avatar for zaru

zaru

November 05, 2024
Tweet

More Decks by zaru

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/ 似たようなライブラリは他にもいくつかある おまけ