🕳️

知らないとあぶない、Next.js セキュリティばなし

2024/06/28に公開

ムーザルちゃんねるのムーです。今回は zaru さんと、Next.js のセキュリティについて話しました。

セキュリティについては様々あると思いますが、今回は以下の3点をピックアップして話しました。

  • Client Components の Props から露出する
  • Server Actions の引数に注意
  • 認証チェックをやってはいけない場所、やって良い場所

これらは、Next.js 入門者がうっかりとやってしまうリスクがあるものです。
このような罠は、アプリケーション自体は正常に動くので、知らないうちにはまってしまいますし、自力で気づくのも難しいものです。もしも知らないものがあれば、ぜひご確認ください。

https://youtu.be/LolugLiLhHs

楽しくて、安全な Next.js 生活をお送りください!

Client Components の Props から露出する

これは、シンプルで当たり前といえば当たり前ですが、Client Component の props はブラウザに露出にしています。ですので、機密情報はのせてはいけません。

特に Next.js では、Serve Component と Client Component が混在しながらコンポーネントを組み合わせていきますので、うっかりと露出させてしまう可能性はあります。

例えば、以下のようなコードはうっかり書かないようにしましょう。

const user = await prisma.users.findUnique({where: {id: userId});

return (
  <div>
    <UserCard user={user} /> // UserCard が Client Component だとすると.. 
  </div>
)

上のようなコードで、UserCard が Client Component だと、user が露出します。場合によっては user.crypted_password などが見れてしまいます。

対策は?

シンプルですが、Client Component の props は必要なものに絞りましょう。

絞るテクニック①

絞る場合は、そもそもデータフェッチする場合に必要なフィールドに絞ると良いでしょう。

公式のブログでも紹介されていますが、
https://nextjs.org/blog/security-nextjs-server-components-actions

DTO(データ転送オブジェクト)を用意すると良いでしょう。僕も zaru さんも DTO を導入しようと思っています。( async function getProfileDTO(slug: string) のようなものです。)個人的には DTO という字面が少し野暮ったい感じで苦手なのですが... 他に良い名前も思いつかないので...。

絞るテクニック② Prisma の場合

Prisma を使っている方にとっては朗報です。この記事を書いているちょうど3日前に v5.16.0 がリリースされまして、グローバルで、特定フィールドを Omit できるようになりました。

https://github.com/prisma/prisma/releases/tag/5.16.0

まだ、preview 機能ですが、とても嬉しい機能なので、僕は早速取り入れました。

const prisma = new PrismaClient({
  omit: {
    user: {
      password: true,
    },
  },
});

上のように書いておくことで、以下のような振る舞いになりますので、うっかりミスをカバーしてくれます。

const user = await prisma.user.findUnique({
  where: { id: id },
  // ここで select を忘れてしまっても...
});

console.log(user.password); // ← undefined になってくれる👍
console.log(user.name); //← 取得できる

嬉しいですね。

Server Actions の引数に注意

Server Actions はとても便利です。Client Component からデータを更新することなどよくあります。

Client Component から Server Actions を呼ぶ場合、それはブラウザからの POST 通信が走ります。ですので、Server Actions の引数を詐称することが可能です。

たとえば、以下のようなコードはあぶないコードになります。

// Client Component にて

const session.userId;

// クリック時に server actions を呼ぶコードイメージ
const handleClick = () => {
  // updateProfile は server actions
  await updateProfile(userId: string, profile: string);
}

ここで、サーバーアクションの updateProfile は Client Component から渡される userId を信じて処理をすると痛い目に遭います。この引数は詐称できてしまうので、適当なユーザーのプロフィールを更新できてしまいます。

対策は?

server actions には、ユーザーに操作されたくない引数は渡さないようにしましょう。

上の例であれば、updateProfile は profile だけを受け付けて、session から取得する userId はサーバーアクション側で取得するようにしましょう。

// 改善前
// 引数で userId を渡してしまっている。
async function updateProfile(userId: string, profile: string) {}

// ↓

// 改善後イメージ
async function updateProfile(profile: string) {
  // 操作されたくない userId は server actions 内部で取得する
  const userId = session.userId

詳しくは以前の zaru さんの記事も参考にしてみてください。

Server Actionsにユーザ操作されたくないデータは渡さない

認証チェックをやってはいけない場所、やって良い場所

これは、少しややこしい話になります。

たとえば、ログインしているユーザーの role が admin かをチェックして、画面アクセスを制御することを想像してみてください。セッションから userId を取得して、場合によっては、それを元に DB の user.role などを確認する場合などがあります。

このとき、このような認証チェック処理は、どこで行うと良いでしょうか?候補は以下のようなものがあります。

  • middleware
  • layout
  • page
  • server actions

結論を言うと

page や server actions で個々にチェックする必要があります。有効な場合は middleware で、cookie レベルのチェックを挟んでおくこともできます。

middleware では不十分で、layout は危険

認証チェックは共通処理ですので、ふつうに考えると、できるだけ共通化されている middleware や layout でやりたくなるとおもいます。ですが、middleware では不十分で、layout は危険なので必ず避けるべきです。

middleware は、Next.js の仕様で、Edge runtime に制限されていて、DB アクセスができません。ですので、認証チェックで DB 確認が必要な場合は利用できません。

そして、layout は罠です。Next.js の rendering 順序が page → layout なので、layout 側で認証チェックを行なっていたとしても、page 側の情報がユーザーに露出されます。(rendering 順序の GitHub 議論

この罠はとても気付きづらいです。 ブラウザで動作確認をすると、しっかりと認証チェックが通ってるような挙動になると思いますが、curl で叩けば、page の情報が取得できてしまいます。

詳しくは以前の zaru さんの記事も参考にしてみてください。

Next.jsのlayout.tsxで認証チェックすると情報漏洩するかも

おわりに

以上となります。上で紹介した罠は、僕はすべてハマりました。

ムーザルちゃんねる

Discussion