{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreida227ngh4nwjuz3mingc5om65wgkr3mxswowlhyxmqlt55icbcxm",
    "uri": "at://did:plc:anldby4lwneunjl777bq6ih7/app.bsky.feed.post/3mkk3wijxr672"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreifpbpyxmloextgu457bggk7i32j74npll3fsuoj2is4o3boqiyluq"
    },
    "mimeType": "image/png",
    "size": 2661111
  },
  "description": "[Tip #129] I love Signed URLs, but there is one very subtle trap you can accidentally fall into...",
  "path": "/security-tip-the-signed-url-trap/",
  "publishedAt": "2026-04-28T07:49:22.000Z",
  "site": "https://securinglaravel.com",
  "tags": [
    "Signed URLs",
    "Magic Login Link",
    "Security Tips",
    "In Depth articles",
    "Laravel Security Audit and Penetration Test",
    "Security Reviews",
    "Bluesky",
    "other socials"
  ],
  "textContent": "`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.\n\nConsider the scenario where you want to provide a Magic Login Link to the user via email. The workflow may look something like this:\n\n  1. User visits `/login` and enters their email address.\n  2. Application stores `user.email=bilbo@baggins.com` in the session, and generates a magic link using: `URL::temporarySignedRoute('login.magic', now()->addMinutes(5))`.\n  3. User receives the link via email and clicks link.\n  4. App validates the Signed URL, and logs user into account matching `user.email`.\n\n\n\nSounds fairly straightforward, right?\n\nThe 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? ๐Ÿคจ**\n\nLet's take a closer look at the Signed URL that gets generated:\n\n\n    https://example.com/login/magic?expires=1777357944&signature=28770f36a0285739e5efcec7fd4cb68fe3a7733b7c171c0fe1b4f61a31861523\n\nNotice the problem yet?\n\nLet's strip off the `expires` and `signature`, as they are added by the Signed URL generator:\n\n\n    https://example.com/login/magic\n\nWhat about now?\n\n**This URL is not unique!**\n\nThere 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.\n\nTo 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.\n\n๐Ÿ’ก\n\n __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.__\n\n**So how do we protect against this?**\n\nThis 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.\n\nThe 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:\n\n\n    > URL::temporarySignedRoute('login.magic', now()->addMinutes(5), ['email' => 'bilbo@baggins.com'])\n\n    = \"https://example.com/login/magic?email=bilbo%40baggins.com&expires=1777359216&signature=2975e64abb35675c54a109cf7e2d769c557d1ff02970363e5ae4576060e0f531\"\n\nAnd 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.\n\nIt 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).\n\n* * *\n\n**_If you found this security tip useful?_ ๐Ÿ‘**\n _Subscribe now_ _to get weekly_ Security Tips_straight to your inbox, filled with practical, actionable advice to help you build safer apps._\n\n**_Want to learn more?_ ๐Ÿค“**\n _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._ ๐Ÿฅฐ\n\n _**Need a second set of eyes on your code?**\nBook in a _Laravel Security Audit and Penetration Test_today! I also offer budget-friendly_ Security Reviews_too._\n\n_Finally, connect with me on_ Bluesky_, or_ other socials_._",
  "title": "Security Tip: The Signed URL Trap",
  "updatedAt": "2026-04-28T07:49:22.685Z"
}