Security Tip: The Signed URL Trap

[Tip #129] I love Signed URLs, but there is one very subtle trap you can accidentally fall into...

Share
Security Tip: The Signed URL Trap

`If you've been following my work for a while, you'll know that I absolutely love Signed URLs, but there is one very subtle trap you may accidently fall into when using them.

Consider the scenario where you want to provide a Magic Login Link to the user via email. The workflow may look something like this:

  1. User visits /login and enters their email address.
  2. Application stores user.email=bilbo@baggins.com in the session, and generates a magic link using: URL::temporarySignedRoute('login.magic', now()->addMinutes(5)).
  3. User receives the link via email and clicks link.
  4. App validates the Signed URL, and logs user into account matching user.email.

Sounds fairly straightforward, right?

The Signed URL prevents the link from being forged or modified, and using user.email from the session ensures the emailed link cannot be hijacked in a different browser or session... or does it? ๐Ÿคจ

Let's take a closer look at the Signed URL that gets generated:

https://example.com/login/magic?expires=1777357944&signature=28770f36a0285739e5efcec7fd4cb68fe3a7733b7c171c0fe1b4f61a31861523

Notice the problem yet?

Let's strip off the expires and signature, as they are added by the Signed URL generator:

https://example.com/login/magic

What about now?

This URL is not unique!

There is nothing in this URL to tie it to the originating user session, which means the Signed URL will work on any pending login session, making it trivial to hijack any user account.

To exploit the attack, all you need to do is initiate a login for an account with an email address you control, which ensures you receive a valid Signed URL. Once you've done that, simply initiate a new login session for your target account, which puts user.email=target@example.com in the session. The existing Signed URL you were sent will still pass validation and the target's email will be inside user.email within your session, letting you walk straight in.

๐Ÿ’ก
Yes, the target will receive the Magic Link email and may get suspicious, but realistically most folks simply ignore these. Plus, even if they do actually do something, the attacker will already be inside the account and likely have already done what they wanted to do.

So how do we protect against this?

This attack isn't just about authentication links, it affects any Signed URLs where the user context/session matters. This could be file downloads, article previews, invite links, etc.

The simplest way to solve it is to inject something within the URL to tie it to the specific user or session. In the case of our magic login link, I would shove the email address in there:

> URL::temporarySignedRoute('login.magic', now()->addMinutes(5), ['email' => 'bilbo@baggins.com'])

= "https://example.com/login/magic?email=bilbo%40baggins.com&expires=1777359216&signature=2975e64abb35675c54a109cf7e2d769c557d1ff02970363e5ae4576060e0f531"

And then after validating the signature in the URL, also validate the email in the URL matches the email in the session user.email. This ensures that the magic link can only ever be used for that specific session for that specific user.

It may feel like redundant information, but it's there for uniqueness. Signed URLs work because the signature verifies that it hasn't been modified, but when the raw URL is /login/magic there is nothing to modify - the signature will always be the same, regardless of context or user. Adding the specific email address makes the URL unique, which changes the signature. Now a URL generated for one user's session won't validate against another. You could add a unique ID, hash, or UUID too - whatever you've got lying around that's unique enough to be validated (and can be exposed to the user).


If you found this security tip useful? ๐Ÿ‘
Subscribe now to get weekly Security Tips straight to your inbox, filled with practical, actionable advice to help you build safer apps.

Want to learn more? ๐Ÿค“
Upgrade to a Premium Subscription for exclusive monthly In Depth articles, or support my work with a one-off tip! Your support directly funds my security work in the Laravel community. ๐Ÿฅฐ

Need a second set of eyes on your code?
Book in a
Laravel Security Audit and Penetration Test today! I also offer budget-friendly Security Reviews too.

Finally, connect with me on Bluesky, or other socials.