求人ボード「RamenHire」に存在した蓄積型HTMLインジェクションの脆弱性により、誰でも公開フォームに入力することで、チームへの通知メールをフィッシングプラットフォームへと変貌させることが可能になっていました。
脆弱性が混入した経緯
RamenHireはNext.jsとSupabaseで動作しており、スタートアップ企業がログインなしで求人を掲載できる仕組みになっています。フォームが送信されるたびに、社内の採用チームへメールが送信されます。メールの本文は、会社名、職務内容、担当者名といったフォームの生の入力フィールドを、直接HTML文字列へと結合することで作成されていました。文字列がメールサービスに到達する前に、エスケープ処理やサニタイズ処理が行われていなかったのです。
テンプレートがユーザー入力を「安全なHTML」として扱っていたため、悪意のある応募者は「ここをクリックして認証してください」といった偽のリンクを注入することができました。このペイロードは求人情報の詳細としてサーバーに保存されるため、その後のすべての通知メールに悪意のあるコードが含まれることになります。攻撃者は、そのリンクを本物の「ダッシュボードを表示」ボタンの隣に隠すことで、チームメンバーを欺いて認証情報を盗み出したり、フィッシングサイトへ誘導したりすることが可能でした。
なぜ重要だったのか
これらのメールは、候補者を採用プロセスに進めるために常にリンクをクリックしている、採用チームの核心メンバーに届くものだったからです。
技術的な欠落
HTMLのエスケープは、単一のステップで完了するものではありません。保護が必要なコンテキストは2つあります。
- テキストノード – タグ間のコンテンツ。
<を<に、>を>に変換することでタグの動作は防げますが、攻撃者が属性値から脱出することを防ぐことはできません。 - 属性値 –
href="…"のようなタグ内の文字列。引用符("または')を注入することで、攻撃者は属性を途中で閉じ、独自のマークアップを挿入できてしまいます。
元のコードはプレーンテキストのみを処理していたため、属性インジェクションに対して無防備な状態でした。
問題の修正
開発者は小さな escapeHtml ヘルパー関数を追加し、値がテキストノードに入るか属性に入るかにかかわらず、補間(interpolation)を行う前にすべてのユーザー提供の値に対してこれを適用しました。
検証手順
- ローカルテスト – 一連のインジェクション文字列をテンプレート関数に入力しました。各テストにおいて、実行可能なタグは現れず、エスケープされた文字のみが表示されることを確認しました。
- 本番環境テスト – パッチをデプロイした後、ライブサイトを通じてペイロードを送信しました(Cloudflare Turnstileのボットチェックを通過)。その結果届いたメールを確認したところ、悪意のあるリンクやスクリプトタグはクリック可能な要素ではなく、単なる文字化けしたテキストとして表示されていました。
Gmailに関する注意点:サービス側でURLを青色にしてクリック可能に表示する場合がありますが、これはクライアント側の利便性によるものであり、HTMLインジェクションが成功したことを意味するものではありません。今回の修正により、インジェクションによって本物のボタンが作成されたり、スクリプトが実行されたりすることは防げます。
開発者が注意すべき点
- 公開フォームからのデータを決して信頼しない – 一見無害に見えるフォームであっても、他の場所で使用される蓄積型の出力(stored output)を生み出す可能性があります。
- 使用するタイミングでエスケープする – パイプラインの早い段階ではなく、レンダリングの直前に、コンテキストに応じたエスケープ(テキストか属性か)を適用してください。
- クライアント側とサーバー側の両方をテストする – ユニットテストで明らかな失敗を検出し、ライブUIを通じた手動のエンドツーエンドテストで、実際のトラフィック下でも防御が機能することを確認してください。
- メールクライアントの癖を意識する – 一部のクライアントはURLを自動的にリンク化するため、誤った安心感を与えることがあります。基盤となるHTMLにアクティブな要素が含まれていないことを確認してください。
まとめ
ユーザー入力の扱いにおけるたった一つの見落としが、日常的な通知メールをフィッシングの経路へと変えてしまいました。包括的なHTMLエスケープ処理を導入し、ローカルおよび本番環境で修正を検証することで、受信トレイの整合性が回復しました。この事例は、信頼できないデータからHTMLを生成するあらゆるWebアプリにとって、時代を超えた教訓を提示しています。適切なサニタイズは譲れない条件であり、それを無視することは、ユーザーの受信トレイへ攻撃者の直通回線を開放することになりかねません。
