📧

【React】React Hook FormとEmailJSを使ったお問い合わせフォーム実装、そしてセキュリティについてちょっとふれる

2024/11/24に公開

前回の記事の引き続きです。
今回は、React Hook Formを活用して作成した入力フォームに、送信ボタンをクリックすると指定したメールアドレスにフォームの内容をメール送信できる機能を追加しました。
この機能を追加するにあたって利用したのはEmailJSというツールです。本記事ではこのEmailJSの設定手順についてまとめたいと思います。

また、記事の最後では、問い合わせフォームに関連するセキュリティ懸念について、セキュリティ未熟者の筆者なりに調べた内容もまとめています。お読みいただけると嬉しいです。

EmailJSとは

EmailJSは、サーバーサイドのコードを必要とせず、クライアントサイドから直接メールを送信できる便利なツールです。特に静的サイトや小規模なアプリケーションに適しており、他のツール(例: SendGridやMailgun)と比べて、初期設定が非常に簡単で、無料プランでも手軽に試せる点が魅力です。
EmailJSではあらかじめメール送信先やメールテンプレートを設定しておき、React側のフォームSubmit時のコールバック関数としてトリガーすることで、簡単にメールを送信できます。
https://www.emailjs.com/
EmailJSには複数の料金プランが用意されています。無料プランの主な制限は以下の通りです。

  • 1ヶ月あたり200リクエストまで
  • 登録可能なメールテンプレートは2つまで
  • 利用履歴のダッシュボード表示は過去7日間分まで
    など制約はありますが、個人開発で利用する分には無料プランでも十分問題ないと思います。

EmailJSの設定

はじめに、EmailJSのユーザー登録を行います。
「Sign up」画面で必要事項を入力すると、登録したメールアドレスに認証確認メールが送信されます。認証後サインインすると以下のようなダッシュボード画面に移ります。
なお、EmailJSの機能でメールを受信するメールアドレスは後の手順で設定できるので、ここで問い合わせフォームの受信者として登録するメールアドレスと同じにしておく必要はありません。

1. Email Serviceの設定

まずは、EmailJSと連携するメールサービスを登録します。「Email Services」の画面から、「Add New Service」をクリックします。
Gmailの他、Outlookなどのメールクライアントを受信先として設定することができます。今回はGmailを利用します。Gmailを選択すると、以下のような入力画面が表示されます。

「Name」には自分がわかる名前を入力します。「ServiceID」は自動発番されますが、自身で任意のものを設定することもできます。
「Connect Account」でアカウントの認証を済ませたら「Create Service」をクリックします。
そうすると以下のように、サービスが登録されます。

ここに表示されている「Service ID」は後ほどReact側で使用します。

2. Email Templateの作成

次にサイドメニューの「Email Templates」をクリックし、「Create New Template」を選択します。以下のような画面が表示されるので、「Content」タブから、入力フォームから入力された内容を元に生成されるお問い合わせメールの雛形を作成します。

「Subject」と「Content」の入力エリアのところにある{{ }}で囲まれた部分に入力フォームから受け取った入力値が反映されます。

ここで、前回作成した入力フォームを確認しておきます。

このフォームのうち、「お名前」、「フリガナ」、「メールアドレス」、「お問合せ内容」の各フィールドに入力された値をそれぞれメールの本文に反映したいので、EmailJS側で受け入れるパラメータ名を以下のように{{ }}で囲みます。

こうすることで、React側からEmailJS側にリクエストを送る際、namefuriganaemailcontentというパラメータ名にReact側の値を割り当てれば、メール本文にその内容を反映できるようになります。

テンプレートを作成後、Saveボタンをクリックするとダッシュボードの「Email Templates」に先ほど作成したテンプレートが追加されているのが確認できます。

ここで表示されている「Template ID」は後ほどReact側で使用します。

3. APIキーを確認する

サイドメニューの「Account」からAPIキーを確認します。

今回はこのうち「Public Key」を使用します。

以上がEmailJS側の設定となります。
ここまでの設定の中で確認できた、以下の3つの情報が以降の設定で必要になりますので、整理しておきましょう。

  • Service ID
  • Template ID
  • Public Key
    これでEmailJSの設定は完了です。次にReact側での設定を進めていきます。

ReactにEmailJSを導入する

ここからはReact側からEmailJSが利用できるよう設定していきます。
導入手順や詳細設定にあたっては以下ドキュメントも合わせてご確認ください。
https://www.emailjs.com/docs/sdk/installation/

1.必要なパッケージをインストールする

まずは、必要パッケージをインストールするために、以下コマンドを実行します。

npm install --save @emailjs/browser

2.環境変数を設定する

先ほど確認できたService IDTemplate IDPublic Keyの3つを環境変数として.envファイルに記述します。(この部分はそれぞれのケースに適した認証情報管理方法に沿ってご対応ください)

REACT_APP_EMAIL_API_KEY=//確認したPublic key//
REACT_APP_EMAIL_SERVICE_ID=//確認したService ID//
REACT_APP_EMAIL_TEMPLATE_ID=//確認したTemplate ID//

3.フォーム送信時のコールバック関数を実装する

前回React Hook Formを使って実装した入力フォームのonSubmit時のコールバック関数を実装します。
現在はconsole.log(data)でフォームの各フィールド入力値をdataオブジェクトとして出力するのみでした。ちなみに、以下にフォーム入力例とそれに対応するdataオブジェクトの中身を示しておきます。

  const onsubmit = async (data: InputTypes) => {
    console.log(data)
  };

フォーム入力例とdata出力内容

これを踏まえて、フォームコンポーネントの内容を以下のように変更します。

import { init, send } from '@emailjs/browser';

// ...

const TestForm = () => {
	const { register, handleSubmit, reset, formState: { errors } } = useForm<InputTypes>();

  const onsubmit = async (data: InputTypes) => {
    // 環境変数から必要なEmailJSのキーを取得
    const userID = process.env.REACT_APP_EMAIL_API_KEY
    const serviceID = process.env.REACT_APP_EMAIL_SERVICE_ID
    const templateID = process.env.REACT_APP_EMAIL_TEMPLATE_ID

    if (userID && serviceID && templateID) {
      // EmailJSを初期化
      init(userID)

      // EmailJSに渡すパラメータを作成
      const params = {
        name: data.name,
        furigana: data.furigana,
        email: data.email,
        content: data.content
      }

      try {
        // メール送信実行
        await send(serviceID, templateID, params, userID)
     // 送信に成功すればダイアログ表示と、フォーム入力内容をリセットする
        alert('この度はお問い合わせ頂き、ありがとうございます。\nお問い合わせを受け付けました。。')
        reset()
      } catch (error) {
     // 失敗した場合のダイアログ表示
        alert('お問い合わせの受付に失敗しました。\n大変恐れ入りますが、しばらく時間をおいてから再度お試しください。')
      }
    }
  }
  
  // ..,
}

関数の構成としては、以下の通りとなります。

  • @emailjs/browserからinit関数と、send関数をインポートします。

  • init関数はEmailJSのサービスを初期化するための関数で、引数にEmailJSのPublic APIキー(userID)を設定し、EmailJS APIと通信する準備を行います。
    今回は環境変数からuserID, serviceID, templateIDがそれぞれ適切に取得できた場合に初期化を行うようにしています。

  • send関数はEmailJSサービスを使ってメールを送信する関数です。引数にサービスID、テンプレートID、テンプレートパラメータ、オプションを指定して呼び出します。
    今回はEmailJSのメールテンプレート側で{{name}}, {{furigana}, {{email}}, {{content}}を用意したのでparamsのプロパティ名はそれぞれname, furigana, email, contentとします。

    そしてそれぞれに対応する値としては、React Hook Formのdataオブジェクトを利用します。
    先ほど確認したdataオブジェクトから、各フィールドに対応する値を抜き出し、それぞれのプロパティに対応させています。

  • send関数の実行結果に応じてtry/catchでそれぞれの実行結果に応じた処理を実装しています。
    処理が成功し無事にメールが送られた場合はその旨を伝えるダイアログ表示と、フォームに入力された内容をクリアするためのreset関数を呼び出しています。このreset関数はReact Hook Formから提供されるもので、useFormから取り出して利用しています。
    なお、エラー時は単純にダイアログを表示するのみとしています。

これで実装は完了となります。
以下のフォームから実際にメール送信を試してみます。

すると、設定した宛先にフワちゃんからメールが届きました!なんか怖い!
うまく受信できない場合は以下の2点で切り分けて考えると良さそうです。

  • フォームの送信ボタンをクリックしても何もおこらない
    if節のuserID && serviceID && templateIDの条件を満たしていないと思われるので、Public KeyService IDなどが環境変数などから適切に渡されているかを確認する。
  • catch節に流れる(404エラー)
    パラメータのプロパティ名がEmailJS側で設定したものと一致しているかどうかを確認する。

セキュリティに関すること

React Hook FormとEmailJSで作るお問合せフォームは、以上でとりあえず動く状態にはなりました。
しかし、ここまでフォームを作成してきた中で、どうしても拭えない漠然とした不安がありました。それがセキュリティについての懸念です。
セキュリティについては学習不足のため、具体的な懸念事項や対策を十分に把握できていませんでした。それでも、「各入力フィールドに適切なバリデーションが設定できているのか」「第三者が入力した内容をそのままメールとして受信して良いのか」「スパム対策はどうすればいいのか」など、漠然とした不安が募っていました。

そのため、この機会にお問い合わせフォームを取り巻くセキュリティ上の懸念事項と対策について簡単に調べてみましたので、以下にまとめておきたいと思います。

まずは入口として懸念すべき事項をChatGPTに聞いてみました。
ChatGPTのアドバイスを踏まえて、今回作成したお問い合わせフォームについて、以下3つの面から評価していきたいと思います。

  1. XSSへの対策
  2. スパム対策
  3. フィッシング対策

1. XSSへの対策

ここで浮かんだ疑問は、「フォームに悪意のあるスクリプトが入力されて送信された場合、受信側のメールクライアントでそのスクリプトが実行される可能性があるのではないか」という点です。
この可能性はゼロではありません。ただし、XSSの発生条件やEmailJSの仕様、使用しているメールクライアントのセキュリティ対策について知ることで、適切な対応ができると考えました。

XSSの発生条件とEmailJS、メールクライアントにおいて講じられている対策

XSSの動作条件
まずは敵を知るところからということで、XSSの発生条件について確認します。

EmailJSにおける安全対策
EmailJSにはテンプレート内で受信データを扱う際にエスケープ処理があるため、通常は安全だそうです。これについて実際に確認してみます。
上記の手順で見てきたようにEmailJSではメールテンプレート内で{{ }}で囲んだ箇所にフォームから入力された内容を反映していました。EmailJSのドキュメントによると、この二重のカッコを三重カッコにするとHTMLを直接埋め込むことが可能になるそうです。実際に試してみると以下の通りでした。
以下に、現在の状態で問い合わせ内容のフィールドにHTMLタグを埋め込んでメール送信してみた結果を示します。

入力したHTMLタグはそのまま文字列として認識されています。

次にメールテンプレートで{{content}}としていた部分を{{{content}}}に変更し、同様の内容を送信してみます。

フワチャンが入力したHTMLがレンダリングされ赤い文字でテキストが送られてきました。怖さが倍増しています。

このように、{{{ }}}で埋め込まれたHTMLは、送信先のメールクライアントでレンダリングされ流ことがわかりました。
つまりこの場合において、先ほど確認したXSSの動作条件を満たしてしまう可能性があります。{{{ }}}の使用についてはEmailJSのドキュメントでもセキュリティ上問題があるとして、注意喚起されています。
https://www.emailjs.com/docs/faq/can-i-send-html-from-my-code/
以上の結果から、EmailJSでメールテンプレートを作成する際は、ユーザー入力値を反映する箇所では{{{ }}}ではなく{{ }}を使用するように心がけていれば、フィールドにHTMLが入力されたとしても、それがそのままの形で送られることはないと考えられます。

メールクライアントでの安全対策
メールクライアント側での安全対策も確認しておきます。
調べてみた限りでは、昨今GmailやOutlookなどのモダンなメールクライアントのほとんどはセキュリティ対策として、受信したメール内の危険なスクリプト(例: <script>タグやイベントハンドラonloadなど)を無効化または削除する仕様になっているそうです。
そのため、HTMLコンテンツをレンダリングする際、直接的なスクリプトが実行されることは実質困難であると考えられます。
この部分についても簡単に試してみます。

今回はEmailJSのテンプレート設定でcontentの部分に、あえて{{{ }}}を仕様し、フィールドに入力されたHTMLはHTMLとして送信されるようにしています。
その上で、以下の内容を入力フィールドに記入し送信してみます。

このように、入力フィールドにscriptタグが埋め込まれていた場合でも、これがそのままGmailクライアントで表示されることはなく、完全に無視されています。

同様に以下のように、イベントハンドラを埋め込んだボタン要素を送信してみます。

メール内容にボタンが表示されていますが、このボタンをデベロッパーツールで確認すると、イベントハンドラが取り除かれたただのボタン要素になっていることがわかります。(画像がわかりにくくてすみません)

次はスクリプト実行を埋め込んだimgタグを埋め込んで送信してみます。

この場合も受信結果には反映されませんでした。
以上から、Gmailでは受信メールに含まれるスクリプトに対して対策されていることがわかりました。

XSSへの対策まとめ

以上から、XSSへの対策についてはEmailJS、Gmailの側でそれぞれ対策が講じられていることがわかりました。利用する側としてはテンプレート作成時に{{{ }}}を使用しないことを徹底すれば、まずは安全に利用することができそうです。
ここからさらに安全を見るのであれば、実装のレベルでは入力した内容がそのまま送信される前に、入力値をサニタイズしHTMLエスケープ処理をしておくといった対応もできそうです。(サニタイズに関連した説明は後述します)

2. スパム対策

スパムは基本的に自動化されたプログラム(ボット)やスクリプトによって大量に送信されます。そのため、スパム対策としてはボットによって行われた問い合わせ送信プロセスを防ぐことができれば効果がありそうです。

スパム対策

スパムの対策にはGoogleが提供するreCAPTCHAというツールが有用です。reCAPTCHAはボット(自動化プログラム)による不正アクセスやスパム送信を防ぐための仕組みです。
そして、EmailJSはreCAPTCHAとの統合をサポートしており、以下のドキュメントで導入手順が説明されています。
https://www.emailjs.com/docs/user-guide/adding-captcha-verification/
設定の流れとしては、

  1. reCAPTCHAのアカウントを作成し、reCAPTCHAを適用するドメイン先の登録などを済ませる。
  2. React側でEmailJSのsendメソッドの引数のparamsにg-recaptcha-response プロパティを追加し、そこにreCAPTCHA側から発行されるトークンを設定する。
  3. EmailJSのメールテンプレート作成画面のSettingタブからreCAPTCHAを有効にする
    と案外簡単そうです。
    実際に導入を進めながら記事に残したいと思ったのですが、Google reCAPTCHA側で適用先のドメインの登録などが必要となるため、導入手順の詳細な説明については今回は割愛させていただきます。
    また改めて試したいと思います。

スパム対策まとめ

スパム対策としてはGoogle reCAPTCHAなどのツールに頼ることで効果的に対策ができそうです。EmailJSとの親和性も高く、実際に公開するWebサイトで問い合わせフォームを作る場合は、ぜひとも導入しておきたいと思いました。

3. フィッシング対策

最後にフィッシング対策について確認しておきます。

https://www.cdnetworks.com/ja/glossary/what-is-a-phishing-attack/

対策

フィッシング対策としては受信者自身の意識向上も不可欠なのですが、今回は入力フォームの側でできそうな対策として、以下2点を実装してみることとしました。

まずは、URLスキームについてです。
URLスキームというのはURLの最初から:までの部分を指すようで、http:https:だけでなくjavascript:data:で始まるものもあるそうです。
https://developer.mozilla.org/ja/docs/Web/URI/Schemes
例えばjavascript:はjavascriptコードを実行するためのスキームで、javascript:alert(”Hello”)で構成されるURLをクリックすると、alert(”Hello”)が実行されます。
今回はこうしたhttp:https:以外のURLがフィールドに入力されていた場合はそれらを排除できるようにしたいと思います。
実装としては入力内容に対して正規表現でURLに該当する部分を検知し、マッチングした部分がhttp:, https:以外のスキームであればそれを””に置き換えることとしました。

次にhttp:, https:で始まるURLに対してrel="noopener noreferrer"の属性を追加します。
それぞれのオプションは以下のような働きをします。

  • noopener : hrefで指定されるリソースにアクセスした際に、開いた元の文書への参照を防ぐことができます。
  • noreferrer : hrefで指定されるリソースにアクセスした際に、リファラー情報が漏洩することを防ぐことができます。リファラー情報とは、現在アクセスしているウェブページが、どのページ(URL)からリンクをたどって訪問されたかを示す情報です。

https://developer.mozilla.org/ja/docs/Web/HTML/Attributes/rel/noopener

https://developer.mozilla.org/ja/docs/Web/HTML/Attributes/rel/noreferrer

https://mathiasbynens.github.io/rel-noopener/

今回はこのURLを変換する機能をカスタムフックとして実装しました。
入力値に対してカスタムフックから取得した変換用関数を実行し、その戻り値をEmailJSのsend関数に渡すようにします。
以下にカスタムフックの実装とそれをEmailJSのsend関数の引数に渡す例を示します。
src/hooks/useUrlSanitize.tsxに実装したカスタムフック

import { useCallback } from "react";

const urlRegex = /([a-zA-Z][a-zA-Z0-9+.-]*:[^\s]+)/g;

// isValidUrl: URLのスキームを確認する関数
const isValidUrl = (url: string): boolean => {
  try {
    const parsedUrl = new URL(url);
    return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:";
  } catch {
    return false;
  }
};

// addRelNoopener: URL部分に rel="noopener noreferrer" を追加する関数
const addRelNoopener = (url: string): string => {
  return `<a href="${url}" rel="noopener noreferrer">${url}</a>`;
};

// カスタムフック
const useUrlSanitizer = () => {
  const sanitizeText = useCallback((inputText: string): string => {
    // URL部分を置き換え処理
    return inputText.replace(urlRegex, (urlMatch) => {
      if (!isValidUrl(urlMatch)) {
        // 無効なURLは空文字に置き換える
        return "";
      }
      // 有効なURLには rel="noopener noreferrer" を付与
      return addRelNoopener(urlMatch);
    });
  }, []);

  return { sanitizeText };
};

export default useUrlSanitizer;

このカスタムフックからsanitizeText関数を取り出し、EmailJSのsend関数に渡す引数に対して関数を実行します。

import { init, send } from '@emailjs/browser';

// ...

const TestForm = () => {
	const { register, handleSubmit, reset, formState: { errors } } = useForm<InputTypes>();

  const onsubmit = async (data: InputTypes) => {
    const userID = process.env.REACT_APP_EMAIL_API_KEY
    const serviceID = process.env.REACT_APP_EMAIL_SERVICE_ID
    const templateID = process.env.REACT_APP_EMAIL_TEMPLATE_ID

    if (userID && serviceID && templateID) {
      init(userID)
      
      // textareaに入力された値に対してカスタムフックから取り出した関数を実行
      const sanitizedText = sanitizeText(data.content)

      const params = {
        name: data.name,
        furigana: data.furigana,
        email: data.email,
        content: sanitizedText // <- 戻り値をsend関数に渡すpramsの値に設定
      }

      try {
        await send(serviceID, templateID, params, userID)
        alert('この度はお問い合わせ頂き、ありがとうございます。\nお問い合わせを受け付けました。。')
        reset()
      } catch (error) {
        alert('お問い合わせの受付に失敗しました。\n大変恐れ入りますが、しばらく時間をおいてから再度お試しください。')
      }
    }
  }
  
  // ..,
}

実際に以下のようなURLを入力し送信してみます。
すると、受信したメールの内容にはjavascript:alert(”hello”)は記載されず、https://google.comのaタグには、rel="noopener noreferrer" の属性が付与されていることが確認できました。

まとめ

前回記事に引き続き、フロントエンドの実装のみでお問い合わせフォームを実装する方法について見てきました。
実装したのはごく簡単なフォームでしたが、それでもセキュリティ上の懸念すべき事項はいくつかあり、今回はその中の一端についてふれてみました。
攻撃手法は日々変化を遂げており、セキュリティについてはいくら対策してもしすぎることはないと思うので、今後もキャッチアップを続けていきたいと思います。
以上、長くなりましたがお読みいただきありがとうございました。ではまた。

Discussion