署名付き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
