追記 2023/10/25
ミドルウェアを使用している際にX-Middleware-Prefetch
ヘッダを付与して送信すると{}
がキャッシュされることで攻撃者は容易にキャッシュを{}
にすることができるためこちらの問題にCVEが付きました。
13.4.20-canary.13
で修正されているのでアップデートすることで解決するかと思います。
3行まとめ
- Next.js 13からMiddlewareを使用していると
getStaticProps
以外を使っていてもprefetchを行うようになった。 - Add middleware prefetching configで
getServerSideProps
のprefetchを行った際のレスポンスボディが{}
となった。 - CDNでキャッシュしている場合、
{}
がキャッシュされてしまうことでページ遷移したときにデータが空になることがある。
前提
Next.js 13 あたりでAdd middleware prefetching configというPRがマージされました。
このPRの意味を理解する前に、現状のNext.jsのprefetchの仕様を理解する必要があります。現在のNext.jsのprefetchではgetStaticProps
で生成された静的ファイルは事前にprefetchを行います*1。getStaticProps
ではビルド時、ISRの際にはいい感じ*2に静的ファイルを作成をするので予めprefetchしてしまうことで高速にページに遷移することが可能となります。
しかし、Middlrewareを使用してパスを書き換えるなどの操作をしている場合、href
に記載されているパスの先はgetStaticProps
を使用しているのかはたまたgetServerSideProps
を使用しているのかわかりません。そのため、この場合はページのSSR/SSG限らずに必ずprefetchを行う仕様*3*4となっているようです。
ですが、この仕様はgetServerSideProps
*5, getInitialProps
で悪影響を及ぼします。prefetchにより意図しないタイミングで実行されたり、クライアントとサーバー両方で実行されてしまうのです。そのため、Middleware affecting getInitialProps after 12.2.0というissueが立ちました。
ちなみに、この問題はNextAuthでも問題になっているようでした。
この修正が、上で述べたPRという訳です。実際にはEnsure prefetch heuristic matches with and without middlewareというPRも前段にあります。変更内容は以下の通りです。
experimental
にmiddlewarePrefetch
という設定を追加middlewarePrefetch
がデフォルトのflexible
では変わらずmiddlewareを使用しているとSSR/SSG限らずprefetchを行うがSSRの場合はgetServerSideProps
を評価せずに{}
をレスポンスで返す*6。middlewarePrefetch
が、struct
の場合はmiddlewareを使用していてもhrefのパスが存在している場合(要はmiddlewareでパスを変更したりリダイレクトしていない場合)、SSGのみprefetchする。
デフォルトでは、middlewarePrefetch
の設定はflexible
*7であるためSSRでもprefetchが飛ぶようになっています。
何が問題か
この「SSRでもprefetchが飛び、レスポンスが{}
で返る」というのが非常に問題となります。
prefetchのパスはhttps://example.com/_next/data/Vkcv8XUXemawzA1A9FCT3/ssr.json
といった実際にそのページに遷移したときにサーバーから取得するパスと同じで、リクエストヘッダーにX-Middleware-Prefetch: 1
という独自ヘッダーを付与してリクエストします。
Next.jsのサーバーはSSRの場合、X-Middleware-Prefetch
ヘッダーがついている場合は{}
を返すといった挙動を取ります。つまり、X-Middleware-Prefetch
のヘッダーの あり/なし でレスポンスデータが大きく変わってしまいます。
CDNを通している場合、/_next/*
もキャッシュしてSSRでもISRみたいな挙動にするといったテクニックが使われていることが多いと思います。その場合、Next.jsのバージョンを13に上げたときにX-Middleware-Prefetch
がCDNのキャッシュキーとなっていないと{}
がキャッシュされるおそれがあります。
これにより、普通にアクセスすると問題ないが<Link>
で遷移すると情報が空っぽになるといった問題*8が発生してしまいます(1敗)。
対策
一番簡単なのはCDNにX-Middleware-Prefetch
をキャッシュキーとして追加することかと思います。
また、アプリケーションレベルでは、middlewarePrefetch
の設定をstruct
にすることでも解決すると思われます。
例
*1:https://taroodr.com/posts/nextjs-prefetch
*2:説明が難しいので各自調べてほしい
*3:https://github.com/vercel/next.js/pull/39920
*4:いつから導入されたのか見つけられませんでしたがどうやら12.2.6-canary.1にはすでに導入されている?
*5:https://github.com/vercel/next.js/issues/39732
*6:SSRもSSGもしていないページのprefetchも{}が返るので合わせたという感じっぽいです。
*7:https://github.com/vercel/next.js/pull/42936/files#diff-1ce54af4c1a0166a265bebae9ba304b24816ebe9cbb92f90b9707379b0680e97R567
*8:https://github.com/vercel/next.js/pull/42936#discussion_r1027563953 PRのコメントでも言及されているがマージ後のコメントのためリアクションが0