質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Next.js

Next.jsは、Reactを用いたサーバサイドレンダリングなどを行う軽量なフレームワークです。Zeit社が開発しており、nextコマンドでプロジェクトを作成することにより、開発環境整備が整った環境が即時に作成できます。

CSRF

クロスサイトリクエストフォージェリ (Cross site request forgeries、CSRF)は、 外部Webページから、HTTPリクエストによって、 Webサイトの機能の一部が実行されてしまうWWWにおける攻撃手法です。

React.js

Reactは、アプリケーションのインターフェースを構築するためのオープンソースJavaScriptライブラリです。

Q&A

解決済

1回答

9025閲覧

Next.jsでのCSRF対策方法を教えてください

sordes1219

総合スコア1

Next.js

Next.jsは、Reactを用いたサーバサイドレンダリングなどを行う軽量なフレームワークです。Zeit社が開発しており、nextコマンドでプロジェクトを作成することにより、開発環境整備が整った環境が即時に作成できます。

CSRF

クロスサイトリクエストフォージェリ (Cross site request forgeries、CSRF)は、 外部Webページから、HTTPリクエストによって、 Webサイトの機能の一部が実行されてしまうWWWにおける攻撃手法です。

React.js

Reactは、アプリケーションのインターフェースを構築するためのオープンソースJavaScriptライブラリです。

1グッド

2クリップ

投稿2021/07/01 09:27

編集2021/07/01 20:38

前提・実現したいこと

Next.jsでStaticGenerationのホームページを作成しています。

ホームページにはContactフォームを設置していて、APIにPOSTすると入力内容をメール送信されるようになっています。機能自体は完成しているのですが、セキュリティ観点でCSRF対策を入れたいと考えています。

方法としては、フォームのページにセッション情報を埋め込んで、POSTリクエスト時にクライアント側のJSからセッション情報を送信して、サーバ側で検証すれば良いのだと思いますが、フォームページにセッション情報を埋め込む方法がわかりません。

Next.jsの公式も読んだのですが、該当箇所がよくわかりませんでした。

全体的な理解が足りていないのかもしれませんが、このあたりの実装方法のヒントや参考文献についてお知恵を拝借できますと助かります。よろしくお願いします。

contact.js

javascript

1import Layout from "../components/layout" 2import Tags from "./tags" 3import SearchWindow from "./search_window" 4import { getUniqTags } from "../lib/posts" 5import axios from 'axios' 6 7export async function getStaticProps() { 8 9 const uniqTags = getUniqTags() 10 11 return { 12 props:{ 13 uniqTags 14 } 15 } 16 17} 18 19const submitFormData = (e) => { 20 e.preventDefault() 21 22 axios.post('/api/mailSender', 23 { 24 'company': company, 25 'nickname': nickname, 26 'email': email, 27 'question': question 28 } 29 ).then(() => { 30 var myModal = new bootstrap.Modal(document.getElementById('myModal')) 31 myModal.toggle() 32 }).catch((error) => { 33 console.log(error) 34 }) 35} 36 37export default function Contact({ uniqTags }) { 38 return( 39 <Layout> 40 <div className="container content"> 41 <div className="row"> 42 <div className="col-md-9 mb-5"> 43 <form onSubmit={submitFormData}> 44 <label htmlFor="company" className="form-label fw-bold">会社名 <span className="badge bg-secondary">任意</span></label> 45 <input maxLength="30" type="text" className="form-control mb-3" id="company" placeholder="会社名を記入してください"/> 46 <label htmlFor="nickname" className="form-label fw-bold">お名前(ニックネーム可) <span className="badge bg-danger">必須</span></label> 47 <input required maxLength="30" type="text" className="form-control mb-3" id="nickname" placeholder="お名前を記入してください"/> 48 <label htmlFor="email" className="form-label fw-bold">Eメールアドレス <span className="badge bg-danger">必須</span></label> 49 <input required maxLength="50" type="email" className="form-control mb-3" id="email" placeholder="メールアドレスを記入してください"/> 50 <label htmlFor="question" className="form-label fw-bold">お問い合わせ内容 <span className="badge bg-danger">必須</span></label> 51 <textarea required maxLength="1000" className="form-control mb-5" id="question" rows="7" placeholder="お問い合わせ、ご相談内容を記入してください"/> 52 <div className="d-flex justify-content-center"> 53 <input id="submitBtn" className="btn btn-success w-50" type="submit" value="この内容で送信する"/> 54 </div> 55 <div className="modal fade" id="myModal" data-bs-backdrop="static" data-bs-keyboard="false" tabIndex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true"> 56 <div className="modal-dialog"> 57 <div className="modal-content"> 58 <div className="modal-header"> 59 <h5 className="modal-title" id="staticBackdropLabel">送信完了</h5> 60 <button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 61 </div> 62 <div className="modal-body"> 63 お問い合わせいただきありがとうございます。 64 </div> 65 <div className="modal-footer"> 66 <button type="button" className="btn btn-secondary" data-bs-dismiss="modal" onClick={closeModal}>Close</button> 67 </div> 68 </div> 69 </div> 70 </div> 71 </form> 72 </div> 73 <div className="col-md-3"> 74 <Tags uniqTags={uniqTags}/> 75 <SearchWindow /> 76 </div> 77 </div> 78 </div> 79 </Layout> 80 ) 81}

/api/mailSender.js

javascript

1import sgMail from '@sendgrid/mail' 2import Cors from 'cors' 3import initMiddleware from '../../lib/init-middleware' 4 5const mailto = process.env.NEXT_PUBLIC_MAILTO 6const mailkey = process.env.NEXT_PUBLIC_MAILKEY 7 8const cors = initMiddleware( 9 Cors({ 10 methods: ['POST'], 11 }) 12) 13 14export default async function handler(req, res) { 15 16 await cors(req, res) 17 18 if (req.method === 'POST') { 19 20 const company = req.body.company 21 const nickname = req.body.nickname 22 const email = req.body.email 23 const question = req.body.question 24 25 sgMail.setApiKey(mailkey) 26 const msg = { 27 to: mailto, 28 from: email, 29 subject: "フォームからのお問い合わせ", 30 html: `<p>お名前:${nickname}</p> 31 <p>会社名:${company}</p> 32 <p>お問い合わせ内容:${question}</p>`, 33 } 34 35 try { 36 await sgMail.send(msg) 37 console.log('Success to send mail') 38 res.status(200).send() 39 } catch(error) { 40 console.log(error) 41 res.status(500).send() 42 } 43 44 } else { 45 res.status(400).send() 46 } 47 48};
退会済みユーザー👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ockeghem

2021/07/01 10:39

情報が不足しています。ソースコードは開示できないのでしょうか。セッション管理の方法はどうやっていますか?
sordes1219

2021/07/01 20:40

早速のコメントありがとうございます。ソースコードを追加しました。contact.jsがContactフォーム表示用、/api/mailSender.jsがメール送信APU用のコードになります。セッションは現場では管理できていません。
guest

回答1

0

ベストアンサー

Next.jsは使ったことがないのですが、回答がつかないようなので、一般論として書きます。

まず前提として、フォームからAPIサーバに送信されるデータはaxios経由でJSON形式で送信されると想定しています。
この場合、Content-Type: application/json となるので、CORSの単純リクエストの要件から外れ、CSRF攻撃の前にプリフライトリクエストが飛びます。これに対してCORSの設定をしていなければPOST送信はされず、CSRF攻撃は成立しません。

ところが、上記前提には色々「抜け道」があります。このあたりがセキュリティの難しいところですね。

まず、Content-Type: text/plainのウェブフォームを使う攻撃があります。以下のようなフォームを使うものです。action属性を正規のAPIに変えて試してみるとよいでしょう。

HTML

1<body> 2 <form action='https://example.jp/api/mailSender' method=post enctype="text/plain"> 3 <input name='{"company":"Sony","nickname":"tom","email":"a@b.com","question":"?' value='"}'> 4 <input type=submit> 5 </form> 6</body>

このフォームによるHTTPリクエストは下記となります(主要部分のみ)。

HTTP

1POST /api/mailSender HTTP/1.1 2Host: example.jp 3Content-Type: text/plain 4Content-Length: 71 5Origin: http://evil.example.com 6Referer: http://evil.example.com/trap.html 7 8{"company":"Sony","nickname":"tom","email":"a@b.com","question":"?="}

enctype=text/plainを悪用して、JSONデータとしてバリッドなものになっています。上記はウェブフォームからのリクエストなのでプリフライトリクエストによる保護はありません。
これの対策は、API側で、HTTPリクエストのContent-Typeがapplication/jsonになっていることを確認することです。

もう一つの可能性は、CORSが無制限に許容されている場合です。その場合は、プリフライトリクエストに対する許可レスポンスが返り、POSTリクエストが送信される結果、CSRF攻撃が成立します。

ここで気になるのは、ご提示いただいたソースコードにcorsの指定がしてあることです。本来、一つのオリジンのみを使用するのであれば、CORSの許可は必要ありません。一方、CORSの許可具合によっては、CSRF攻撃が成立します。

まとめると、以下がしてあれば、CSRF攻撃はできません。

  • JSONデータを受け取る際にContent-Typeがapplication/jsonであることを確認している
  • CORSの設定をしていないか、正しいオリジンのみを受け入れるように設定している

CSRF攻撃が成立するかどうかを試すことは簡単です。以下のようなJavaScriptをAPIサーバとは別のサーバにおいて、実行してみることです。先のenctype="text/plain"のウェブフォームも試してみることをお勧めします。

axios.post('https://example.jp/api/mailSender', { 'company': 'CSRF', 'nickname': 'Alice', 'email': 'hoge@example.jp', /* 受取可能なメールアドレスにしておく */ 'question': 'No question' } ).then(() => { console.log('Success') }).catch((error) => { console.log(error) })

投稿2021/07/03 03:53

ockeghem

総合スコア11705

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sordes1219

2021/07/04 05:19

丁寧な回答ありがとうございました。よくわかりました。 テストプログラムでContent-Type: text/plainの場合ではプリフライトが飛ばないことも確認しました。axiosでoriginヘッダーを偽装したらどうなるかも、試してみましたが、axiosでは書き換えできない仕様となっていました。
ockeghem

2021/07/04 05:23

originヘッダはセキュリティ上の理由でJavaScript(jQuery、axios等も含めて)では改変できない仕様です。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問