Security issue with magic-link #1802

Closed
opened 2026-03-13 09:04:33 -05:00 by GiteaMirror · 7 comments
Owner

Originally created by @maelp on GitHub (Aug 28, 2025).

Originally assigned to: @ping-maxwell on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

I'm wondering if the current implementation of magic-link doesn't have a subtle security issue, in the sense that the CLIENT can decide what are the callbackUrl

I think it should just be parameterized on the backend for safety so it can't be manipulated

We could have a way to give a parameter in the client if we have multiple apps, in order to know to which we want to authenticate (eg POST /sign-in/magic-link with data {email, app: "my-app"})

here is the "attack" I was thinking about:

  • ATTACKER knows that BOB is a potential user of MYAPP
  • ATTACKER asks MYAPP for a magic link, but provides ATTACKER-DOMAIN as a callback-url (perhaps a typo-squatted site very close to MYAPP, but mostly people don't really look at link HREFs in HTML emails)
  • ATTACKER creates a clone of MYAPP website (same layout / graphics etc) at ATTACKER-DOMAIN and provides a "Set a new password" page
  • BOB is redirected there and thinks it's the legit MYAPP
  • BOB enters his password (or some password that he wanted to use for MYAPP)

--> if BOB doesn't use a random password using a password manager, but re-uses the same password, or had created before an account with password on MYAPP and was tricked into thinking ATTACKER-DOMAIN is the same as MYAPP, he leaked his credentials

Current vs. Expected behavior

Currently: the client can choose any callback URL allowing to redirect the link to any domain

Expected: only the backend can choose the callback URL

What version of Better Auth are you using?

latest

System info

Latest

Which area(s) are affected? (Select all that apply)

Package

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

No response

Originally created by @maelp on GitHub (Aug 28, 2025). Originally assigned to: @ping-maxwell on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce I'm wondering if the current implementation of magic-link doesn't have a subtle security issue, in the sense that the CLIENT can decide what are the callbackUrl I think it should just be parameterized on the backend for safety so it can't be manipulated We could have a way to give a parameter in the client if we have multiple apps, in order to know to which we want to authenticate (eg POST /sign-in/magic-link with data {email, app: "my-app"}) here is the "attack" I was thinking about: - ATTACKER knows that BOB is a potential user of MYAPP - ATTACKER asks MYAPP for a magic link, but provides ATTACKER-DOMAIN as a callback-url (perhaps a typo-squatted site very close to MYAPP, but mostly people don't really look at link HREFs in HTML emails) - ATTACKER creates a clone of MYAPP website (same layout / graphics etc) at ATTACKER-DOMAIN and provides a "Set a new password" page - BOB is redirected there and thinks it's the legit MYAPP - BOB enters his password (or some password that he wanted to use for MYAPP) --> if BOB doesn't use a random password using a password manager, but re-uses the same password, or had created before an account with password on MYAPP and was tricked into thinking ATTACKER-DOMAIN is the same as MYAPP, he leaked his credentials ### Current vs. Expected behavior Currently: the client can choose any callback URL allowing to redirect the link to any domain Expected: only the backend can choose the callback URL ### What version of Better Auth are you using? latest ### System info ```bash Latest ``` ### Which area(s) are affected? (Select all that apply) Package ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context _No response_
GiteaMirror added the security label 2026-03-13 09:04:33 -05:00
Author
Owner

@ping-maxwell commented on GitHub (Aug 28, 2025):

There are origin checks in place

Image
@ping-maxwell commented on GitHub (Aug 28, 2025): There are origin checks in place <img width="696" height="366" alt="Image" src="https://github.com/user-attachments/assets/b5c94f42-1705-4621-b505-71bbc140799b" />
Author
Owner

@maelp commented on GitHub (Aug 28, 2025):

@ping-maxwell nice, can you explain to me how I set them up/ how they work?

on the issue, a way to mitigate would be to just define callbackUrl="my-app" by convention in the client, and in the backend for sendEmail, if we see callbackUrl="my-app" exactly, we replace it with the true callbackUrl, and if we see anything else, we return an error

@maelp commented on GitHub (Aug 28, 2025): @ping-maxwell nice, can you explain to me how I set them up/ how they work? on the issue, a way to mitigate would be to just define callbackUrl="my-app" by convention in the client, and in the backend for sendEmail, if we see callbackUrl="my-app" exactly, we replace it with the true callbackUrl, and if we see anything else, we return an error
Author
Owner

@ping-maxwell commented on GitHub (Aug 28, 2025):

It checks the passed callbackURL's origin against your auth config's trustedOrigins list.
For stricter pathname specific checks, I would recommend using hooks

@ping-maxwell commented on GitHub (Aug 28, 2025): It checks the passed callbackURL's origin against your auth config's `trustedOrigins` list. For stricter pathname specific checks, I would recommend using hooks
Author
Owner

@maelp commented on GitHub (Aug 28, 2025):

@ping-maxwell thanks for the precisions! Can you just tell me what hooks you're thinking about, and how I would use them? (point me to the source code or the doc?)

@maelp commented on GitHub (Aug 28, 2025): @ping-maxwell thanks for the precisions! Can you just tell me what hooks you're thinking about, and how I would use them? (point me to the source code or the doc?)
Author
Owner

@maelp commented on GitHub (Aug 28, 2025):

BTW can this magic-link setup work with Capacitor apps?

@maelp commented on GitHub (Aug 28, 2025): BTW can this magic-link setup work with Capacitor apps?
Author
Owner

@ping-maxwell commented on GitHub (Aug 28, 2025):

Can you just tell me what hooks you're thinking about, and how I would use them? (point me to the source code or the doc?)

hooks docs: https://www.better-auth.com/docs/concepts/hooks#before-hooks
Create a before hook, then use an if statement to check if the request is the/sign-in/magic-link path or /magic-link/verify path.
Then get the ctx.body (or ctx.query for the /verify endpoint) to grab the params and then find the callbackURL from the body/query, to then run your more specific checks. If the callbackURL doesn't pass your checks, you can then throw an APIError.

@ping-maxwell commented on GitHub (Aug 28, 2025): > Can you just tell me what hooks you're thinking about, and how I would use them? (point me to the source code or the doc?) hooks docs: https://www.better-auth.com/docs/concepts/hooks#before-hooks Create a before hook, then use an if statement to check if the request is the`/sign-in/magic-link` path **or** `/magic-link/verify` path. Then get the `ctx.body` (or `ctx.query` for the /verify endpoint) to grab the params and then find the `callbackURL` from the body/query, to then run your more specific checks. If the callbackURL doesn't pass your checks, you can then throw an APIError.
Author
Owner

@ping-maxwell commented on GitHub (Aug 28, 2025):

BTW can this magic-link setup work with Capacitor apps?

Not too sure sorry, I've never setup a Capacitor app

@ping-maxwell commented on GitHub (Aug 28, 2025): > BTW can this magic-link setup work with Capacitor apps? Not too sure sorry, I've never setup a Capacitor app
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1802