{"id":"GHSA-hwq2-v29r-p69r","kind":"vulnerability","aliases":["GHSA-hwq2-v29r-p69r"],"related":[],"title":"better-auth: Email-change links stay valid after the address changes hands","description":"## Am I affected?\n\nYou are affected if you use `better-auth` 1.4.0 or later with **all** of these:\n\n- `user.changeEmail.enabled` is turned on.\n- `emailVerification.sendVerificationEmail` is configured.\n- A new user can register an address after its previous owner moves to another one.\n\n## Summary\n\nAn email change link recorded the old and new addresses, but not the user who asked for the change. If someone else registered the old address while the link was still valid, opening the link acted on that person's account.\n\n## Impact\n\nA user who controls the addresses involved can take over an account that later registers their released old address, using a link issued for their own account. They get a session for that account, and its email changes to an address they control. The attack needs the victim to register the released address while one of the attacker's links is still valid.\n\n## Patches\n\nUpgrade `better-auth` to 1.7.16, or to 1.8.0-beta.5 on the beta line.\n\nEmail verification and change links now work only for the account they were issued to. Opening a change link no longer signs the user in, and it marks the new address verified only if the current one was verified.\n\n## Workarounds\n\nTurn off user-initiated email changes until you upgrade. A lower `emailVerification.expiresIn` shortens the window but does not stop the attack.\n\n## Technical details\n\nThe change link carried the old and new addresses. Without a session, Better Auth looked up the account that owned the old address at that moment, created a session for it, and changed its email. The link now carries the requesting user's ID and is rejected for any other account.","summary":"Email-change links in better-auth did not record which user requested the change, so a link could act on whichever account later registered the old address. An attacker who controls both addresses can take over that new account and change its email to one they control.","severity":"medium","cvss":6.4,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"better-auth","fixed":["1.7.16"],"affected":">= 1.4.0, < 1.7.16","ecosystem":"npm"},{"name":"better-auth","fixed":["1.8.0-beta.5"],"affected":">= 1.8.0-beta.0, < 1.8.0-beta.5","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["better-auth","cwe-287","cwe-294","node","package","self-hosted"],"not_affected_if":["changeemail-disabled","no-email-verification","pre-1.4.0"],"how_to_check":"- Check the installed version: `npm ls better-auth`\n- Look for `user.changeEmail.enabled` set to true in the auth configuration\n- Confirm `emailVerification.sendVerificationEmail` is configured","fix":"- `better-auth` 1.4.0 up to but not including `1.7.16`: move to `1.7.16`\n- `better-auth` 1.8.0-beta.0 up to but not including `1.8.0-beta.5`: move to `1.8.0-beta.5`\n- Until upgraded, the advisory says to turn off user-initiated email changes; a lower `emailVerification.expiresIn` does not stop the attack","status":"open","published_at":"2026-11-03T22:07:41+00:00","first_seen_at":"2026-11-09T22:53:20+00:00","triaged_at":"2026-11-09T22:54:44+00:00","updated_at":"2026-11-09T22:54:44+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-11-09T22:54:44Z","model":"claude-haiku-5-5","output":{"fix":"- `better-auth` 1.4.0 up to but not including `1.7.16`: move to `1.7.16`\n- `better-auth` 1.8.0-beta.0 up to but not including `1.8.0-beta.5`: move to `1.8.0-beta.5`\n- Until upgraded, the advisory says to turn off user-initiated email changes; a lower `emailVerification.expiresIn` does not stop the attack","reach":"internet","title":"better-auth: email-change links stay valid after the address changes hands","impact":"high","summary":"Email-change links in better-auth did not record which user requested the change, so a link could act on whichever account later registered the old address. An attacker who controls both addresses can take over that new account and change its email to one they control.","audience":"developers","category":"package","nicknames":[],"who_cares":["better-auth","node","self-hosted"],"confidence":0.85,"fix_status":"available","needs_auth":"yes","worst_case":"attacker takes over a new account and sets its email","analyst_note":"","how_to_check":"- Check the installed version: `npm ls better-auth`\n- Look for `user.changeEmail.enabled` set to true in the auth configuration\n- Confirm `emailVerification.sendVerificationEmail` is configured","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["changeEmail-disabled","no-email-verification","pre-1.4.0"],"who_is_affected":"Applications using better-auth 1.4.0 or later with user-initiated email changes enabled and email verification sending configured. The exposure needs a released email address that a new user can register while an old change link is still valid; apps without changeEmail enabled can skip it.","user_interaction":"yes","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"unknown"},"cost_usd":0.00082,"input_tokens":5065,"output_tokens":627,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-287':35B '-294':37B '1.4.0':104C '1.7.16':245C '1.8.0':248C 'account':66B,83B,174C,187C,202C,209C,264C,335C,368C 'act':63B,169C 'address':12A,71B,77B,124C,143C,159C,181C,194C,216C,228C,284C,326C,340C 'affect':94C,97C 'anoth':131C 'ask':149C 'attack':73B,220C,233C,315C 'auth':3A,27A,30A,33B,50B,103C,243C,331C 'beta':252C 'beta.5':249C 'better':2A,26A,29A,32B,49B,102C,242C,330C 'better-auth':1A,25A,28A,31B,48B,101C,241C 'carri':321C,356C 'chang':6A,13A,45B,58B,85B,136C,152C,213C,257C,271C,300C,319C,350C 'configur':117C 'control':75B,91B,179C,218C 'could':62B 'creat':344C 'current':289C 'cwe':34B,36B 'detail':317C 'els':155C 'email':5A,44B,87B,135C,212C,254C,299C,352C 'email-chang':4A,43B 'emailverification.expiresin':306C 'emailverification.sendverificationemail':115C 'get':204C 'ghsa':16A,21A 'ghsa-hwq2-v29r-p69r':15A,20A 'hand':14A 'host':42B 'hwq2':17A,22A 'id':361C 'impact':175C 'initi':298C 'involv':182C 'issu':198C,267C 'later':67B,106C,189C 'line':253C 'link':7A,46B,61B,137C,162C,168C,197C,235C,258C,272C,320C,354C 'longer':274C 'look':332C 'lower':305C 'mark':281C 'moment':343C 'move':129C 'need':221C 'new':82B,119C,142C,283C,325C 'node':38B 'old':70B,140C,158C,193C,323C,339C 'one':89B,132C,230C,290C 'open':166C,269C 'own':337C 'owner':128C 'p69r':19A,24A 'packag':39B 'patch':239C 'person':172C 'previous':127C 'record':53B,138C 'regist':68B,122C,156C,190C,225C 'reject':364C 'releas':192C,227C 'request':56B,358C 'self':41B 'self-host':40B 'session':206C,329C,346C 'shorten':307C 'sign':275C 'someon':154C 'stay':8A 'still':164C,237C 'stop':313C 'summari':133C 'take':79B,184C 'technic':316C 'turn':113C,294C 'upgrad':240C,303C 'use':100C,195C 'user':55B,120C,147C,177C,277C,297C,359C 'user-initi':296C 'user.changeemail.enabled':111C 'v29r':18A,23A 'valid':9A,165C,238C 'verif':255C 'verifi':285C,292C 'victim':223C 'whichev':65B 'window':309C 'without':327C 'work':260C 'workaround':293C","nicknames":[],"mitigations":[],"who_affected":"Applications using better-auth 1.4.0 or later with user-initiated email changes enabled and email verification sending configured. The exposure needs a released email address that a new user can register while an old change link is still valid; apps without changeEmail enabled can skip it.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["betterauth"],"ptokens":["auth","better"],"labels":["betterauth","cwe287","cwe294","medium","node","npm","package","selfhosted"],"campaign_type":null,"search_a":"'address':12A 'auth':3A,27A,30A 'better':2A,26A,29A 'better-auth':1A,25A,28A 'chang':6A,13A 'email':5A 'email-chang':4A 'ghsa':16A,21A 'ghsa-hwq2-v29r-p69r':15A,20A 'hand':14A 'hwq2':17A,22A 'link':7A 'p69r':19A,24A 'stay':8A 'v29r':18A,23A 'valid':9A","search_b":"'-287':35B '-294':37B 'account':66B,83B 'act':63B 'address':71B,77B 'attack':73B 'auth':33B,50B 'better':32B,49B 'better-auth':31B,48B 'chang':45B,58B,85B 'control':75B,91B 'could':62B 'cwe':34B,36B 'email':44B,87B 'email-chang':43B 'host':42B 'later':67B 'link':46B,61B 'new':82B 'node':38B 'old':70B 'one':89B 'packag':39B 'record':53B 'regist':68B 'request':56B 'self':41B 'self-host':40B 'take':79B 'user':55B 'whichev':65B","search_c":"'1.4.0':104C '1.7.16':245C '1.8.0':248C 'account':174C,187C,202C,209C,264C,335C,368C 'act':169C 'address':124C,143C,159C,181C,194C,216C,228C,284C,326C,340C 'affect':94C,97C 'anoth':131C 'ask':149C 'attack':220C,233C,315C 'auth':103C,243C,331C 'beta':252C 'beta.5':249C 'better':102C,242C,330C 'better-auth':101C,241C 'carri':321C,356C 'chang':136C,152C,213C,257C,271C,300C,319C,350C 'configur':117C 'control':179C,218C 'creat':344C 'current':289C 'detail':317C 'els':155C 'email':135C,212C,254C,299C,352C 'emailverification.expiresin':306C 'emailverification.sendverificationemail':115C 'get':204C 'id':361C 'impact':175C 'initi':298C 'involv':182C 'issu':198C,267C 'later':106C,189C 'line':253C 'link':137C,162C,168C,197C,235C,258C,272C,320C,354C 'longer':274C 'look':332C 'lower':305C 'mark':281C 'moment':343C 'move':129C 'need':221C 'new':119C,142C,283C,325C 'old':140C,158C,193C,323C,339C 'one':132C,230C,290C 'open':166C,269C 'own':337C 'owner':128C 'patch':239C 'person':172C 'previous':127C 'record':138C 'regist':122C,156C,190C,225C 'reject':364C 'releas':192C,227C 'request':358C 'session':206C,329C,346C 'shorten':307C 'sign':275C 'someon':154C 'still':164C,237C 'stop':313C 'summari':133C 'take':184C 'technic':316C 'turn':113C,294C 'upgrad':240C,303C 'use':100C,195C 'user':120C,147C,177C,277C,297C,359C 'user-initi':296C 'user.changeemail.enabled':111C 'valid':165C,238C 'verif':255C 'verifi':285C,292C 'victim':223C 'window':309C 'without':327C 'work':260C 'workaround':293C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-11-03T22:07:41+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-hwq2-v29r-p69r","url":"https://github.com/better-auth/better-auth/security/advisories/GHSA-hwq2-v29r-p69r","severity":"medium","published_at":"2026-11-03T22:07:41+00:00","modified_at":"2026-11-03T22:07:41+00:00","first_seen_at":"2026-11-09T22:53:20+00:00","fetched_at":"2026-11-09T22:53:20+00:00","note":null}],"documents":[{"id":110497,"url":"https://github.com/better-auth/better-auth/security/advisories/GHSA-hwq2-v29r-p69r","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}