署名付きURLを壊してしまうトラッキングリンクのバグ

最も重要なものだけを壊してしまうバグは、非常に危険です。

mail-historyにおいて、通常のリンクでは完璧に動作するものの、署名付きURL(signed URLs)では失敗するというバグを見つけました。これにより、メール認証リンクや署名付きダウンロードリンクが壊れてしまいます。これらは、一文字でも間違っているとLaravelがリクエストを拒否してしまうようなリンクです。

発生した経緯は以下の通りです。

mail-historyは、HTMLを書き換えることでメールのクリックを追跡します。すべてのリンクを、まずリダイレクトエンドポイントを経由するように変更します。このエンドポイントがクリックを記録し、その後ユーザーを実際の遷移先に送ります。

これを実現するために、システムは元のURLを暗号化してトラッキングリンクにします。

問題は、システムがレンダリングされたHTMLからURLを取得する際に発生します。コードがメール本文を読み取る頃には、LaravelはすでにHTMLのエスケープ処理を終えています。リンク内のアンパサンド (&) は & になります。

https://example.com/page のような通常のリンクは、アンパサンドを含まないため問題なく動作します。

しかし、署名付きURLは次のようになります: https://example.com/email/verify/1/abc?expires=123&signature=deadbeef

HTML内では、次のようになります: https://example.com/email/verify/1/abc?expires=123&signature=deadbeef

コードはその & という文字列を暗号化してしまいます。ユーザーがクリックすると、システムはそれを復号し、& を含むURLへとユーザーを飛ばします。Laravelは署名の検証を試みますが、文字がオリジナルと一致しないため、署名検証に失敗します。

修正はわずか一行のコードで済みます。URLを暗号化する前に、HTMLエンティティをデコードする必要があります。

$originalUrl = html_entity_decode($matches[2], ENT_QUOTES | ENT_HTML5);

これにより、暗号化が行われる前に && に戻ります。復号されたリンクは、元の署名付きURLとバイト単位で完全に一致するようになります。

再発を防ぐためにテストも追加しました。このテストは、復号されたURLに & ではなく、実際の & が含まれているかを確認します。

このような小さな修正は、将来のコードクリーンアップの際に失われがちです。常に、特定の失敗内容を明示したテストを書いておきましょう。

教訓:

  • レンダリングされたHTMLからデータを抽出する場合は、エスケープされているものと想定してください。
  • ブラウザはエスケープされた文字を自動的に修正してくれますが、暗号化やリダイレクトはそうしてくれません。
  • 1行の修正であっても、テストを使って保護しましょう。

Source: https://dev.to/nasrulhazim/the-tracking-link-bug-that-only-breaks-signed-urls-38c