Discussion
セキュリティ
機密性の高い情報が意図に反してブラウザに流れてしまうリスクを取り上げます。これはNext.jsの公式ブログの記事、"How to Think About Security in Next.js"と同じ関心領域です。ムーザルちゃんねるでも取り上げられており、Client ComponentsのPropsからデータが露出する問題が紹介されています。
実際のデモを見ながら、具体的に紹介していきます。
Next.jsでは、Client Componentが含まれる場合、あるいはPages RouterでSSR/SSGをしている場合(つまり純粋にServer Componentだけを使っている以外の場合)はデータ漏洩の注意が必要です。注意がおろそかだと機密情報をブラウザに送ってしまい、それに気づかないケースが起こり得ます。
下記はPages RouterでSSGを行った場合のコードです。allUser()
関数の返り値をそのままpropsとしてコンポーネントに渡してしまっています。そしてこの中には機密情報の"password_digest"が漏洩してしまっています。
pages/users_ssg/index.tsx
export async function getStaticProps() {
const users = await allUsers()
return {props: {users}}
}
次にブラウザでSSGのページに訪問し、開発者用のネットワークタブから/users_ssg
へのリクエスト結果を確認します。そしてHTMLの一番最後にあるscript
タグの中を確認すると、"password_digest"の中身が見えます(下図:見やすく整形してあります)。もちろん画面には"password_digest"が表示されていませんので、開発者もQAも漏洩に気づかない恐れがあります。
これを防ぐには、コードレビュー等をしっかり行い、データベースから取得されたUser情報がそのままコンポーネントに渡されないように注意する必要があります。
開発者用のネットワークタブから/users_ssg
を確認した例
<script id="__NEXT_DATA__" type="application/json">
{
"buildId": "tfKzayy7vVgrY4oirxrPI",
"gsp": true,
"isFallback": false,
"page": "/users_ssg",
"props":
{
"__N_SSG": true,
"pageProps":
{
"users":
[
{
"email": "hogeta@example.com",
"id": 1,
"name": "Hogeta Hogeo",
"password_digest": "dbfcfd0d87220f629339bd3adcf452d083fde3246625fb3a93e314f833e20d37",
"role": "Member",
"title": "Front-end developer"
},
{
"email": "hogeko@example.com",
"id": 2,
"name": "Hogehara Hogeko",
"password_digest": "4bdd0bbfe3f4c52cc2c8ff02f1fef29663dd9938f230304915805af1fa71e968",
"role": "Member",
"title": "Designer"
},
...
]
}
},
"query":
{},
"scriptLoader":
[]
}
</script>
Hotwire等のhydration不要なSSR技術では、HTMLのみをブラウザに送ります。送信されるのは画面に表示される内容だけです(上図で言うと、"password_digest"はブラウザに送られません)。万が一、意図しないデータをブラウザに送ったとしても、開発者もしくはQAはすぐに気づきますので、データ漏洩のリスクは少ないと言えます。
Next.jsの場合も、Server Componentに限り同様のことが言えます。Server ComponentはHTMLの他に"RSC payload"というものがブラウザに送信されますが、画面に表示される内容しか含まれておらず、ほぼHTMLと同じように考えることができるためです。ただしServer Componentの下流にClient Componentがある場合、これはHydrationを要しますので、データ漏洩のリスクが復活してしまいますので、これに頼るのは危険そうです。
この場合はHTMLテンプレート自体がセキュリティレイヤーとなり、安全な情報のみが選別され、チェックも行われると考えて良いでしょう。
上記でも触れましたが、本デモでは、敢えてセキュリティ上問題のあるコードをバックエンドで書いています。セキュリティの問題を浮き彫りにするためです。
具体的にはUser repository
がそのままpassword_digest
(秘密の情報)を返してしまうようにしています。また各エンドポイントでもpassword_digest
をブロックする処理を行っていません。
password_digest
は漏洩しません。レスポンスにはHTMLしか含まれないので、画面に表示しない内容はブラウザに送信されないためです。ブラウザの検証画面のネットワークタブを確認し、送信されてくる各ファイルの中身を見ても、password_digest
の情報はありません。/api/users
からのJSONレスポンスにpassword_digest
が出てしまうように作ってありますので、ここから漏洩します。しかしこのようなAPIはOpen APIでドキュメントをやり取りしながら、注意深く設計されることがほとんどだと思いますので、問題にはなりにくいと考えられます。script
タグ中にpassword_digest
が漏洩します。これはhydration用のデータであり、HTMLにpassword_digest
がレンダリングされるかどうかに関わらず含まれます。またページ遷移をするたびにダウンロードされるJSONファイルにも漏洩します。password_digest
は漏洩しません。RSC
payloadはHTMLにレンダリングされる内容しか含まないためです。しかしServer componentの中にClient
componentを埋め込んでいる場合はそこがHydrationされますので、データが漏洩する可能性があります。要注意です。Next.jsの場合は、コンポーネントに機密性のあるデータを渡さないことが対策になります。純粋なSPAのReactではサーバとブラウザが完全に別れており、セキュリティに注意しながらOpen API等でAPIを設計していました。APIで十分に気を使うことで、情報漏洩を防ぐことができていました。
しかしNext.jsのSSG/SSR、さらにRSCのおかげでサーバとブラウザの境界はより自由に行き来できるようになりました。セキュリティに注意を払う場面が減りました。あえてレイヤーを挟む対策がより重要になりました。
例えばUser repository
のデータをそのままコンポーネントに渡さず、Data Access
Layerを作り、この中で権限に応じて必要なデータのみを含むDTO(Data
Transfer Object)を作成することがNext.jsの公式ページで奨励されています。
Data Access Layerを作るのが大変すぎるのであれば、Lodashのpick()
を使い、より簡便に情報漏洩を防ぐ方法が良いのではないかと思います。
Reactの方でもReact Taint APIで対策されていく見込みです。ただしこれはどちらかというと注意喚起のメカニズムだけであり、実際の対応は別途必要になりそうです。
一方でHotwireの場合は、HTMLを出力するテンプレートファイル自身がこのようなData Access Layerの役割を果たしているとも言えます。不要な情報が漏れ出ている場合は画面でもすぐに確認できますので、安全性が高いと言えます。さらにViewのユニットテストを書けば、より堅牢になります。
結論としてMPAやTurbo Driveを使用するときに比べ、Next.jsはデータ漏洩に神経を使う必要がありそうです。