動かざることバグの如し

近づきたいよ 君の理想に

Next.jsでCSRF対策を簡単に実装する方法

環境

  • Nextjs 13
  • edge-csrf 1.0.3-rc1
  • TypeScript 5

やりたいこと

今日は、Next.jsの開発について書こうと思います。Next.jsは、開発がとても便利であることがよく知られています。

しかし、CSRFトークンの機能がRailsのようにネイティブで用意されていないため、自分で実装する必要があります。

これは面倒ですが、edge-csrfというライブラリを使えば簡単にCSRFトークンの実装ができます。先日、私自身もこのライブラリを使ってCSRFトークンの実装を行い、スムーズに開発を進めることができました。

今回はそのメモ

https://github.com/amorey/edge-csrf

インストール

npm install edge-csrf
yarn add edge-csrf

ミドルウェアの追加

APIのエンドポイントごとにCSRFの実装するのは流石に面倒。そこでNextjsのミドルウェア機能を使う。これを使うとAPIリクエストごとに自動でCSRFトークンをチェックしてくれる

プロジェクト直下にmiddleware.tsを作成 人によってはsrc直下になるのかな

import csrf from 'edge-csrf';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// initalize protection function
const csrfProtect = csrf({
  cookie: {
    secure: process.env.NODE_ENV === 'production',
  },
});

export async function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // csrf protection
  const csrfError = await csrfProtect(request, response);

  // check result
  if (csrfError) {
      return new NextResponse('invalid csrf token', { status: 403 });
  }
    
  return response;
}

フロントエンドの実装

あとは以下のようなフォームを pages/form.ts とかに作成して

import type { NextPage, GetServerSideProps } from 'next';
import React from 'react';

type Props = {
  csrfToken: string;
};

export const getServerSideProps: GetServerSideProps = async ({ res }) => {
  const csrfToken = res.getHeader('x-csrf-token') || 'missing';
  return { props: { csrfToken } };
}

const FormPage: NextPage<Props> = ({ csrfToken }) => {
  return (
    <form action="/api/form-handler" method="post">
      <input type="hidden" value={csrfToken}>
      <input type="text" name="my-input">
      <input type="submit">
    </form>
  );
}

export default FormPage;

バックエンドの実装

pages/api/form-handler.tsを以下のように用意すると

import type { NextApiRequest, NextApiResponse } from 'next';

type Data = {
  status: string
};

export default function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
  // this code won't execute unless CSRF token passes validation 
  res.status(200).json({ status: 'success' });
}

確認

<input type="hidden" value={csrfToken}>

のようにセットされていればフォームの送信は成功するが

<input type="hidden" value='invalid_token'>

のようになっていると送信に失敗するはず

またcurlで試しても失敗すればおk

❯ curl -XPOST http://localhost:3000/api/form-handler/
invalid csrf token

オプション

クッキーや挙動の設定は公式ドキュメント参照

サンプル

公式Gitにサンプルプロジェクトがあるのでこれが一番参考になるかも

https://github.com/amorey/edge-csrf/tree/main/example-ts

自分用にMantineのFormを利用したバージョンのサンプルも作成した

https://github.com/thr3a/nextjs-csrf-example