[{"data":1,"prerenderedAt":579},["ShallowReactive",2],{"blog-vulnerabilities/better-auth-pre-account-hijacking":3},{"id":4,"title":5,"body":6,"category":547,"date":548,"dateModified":548,"description":549,"draft":550,"extension":551,"faq":552,"featured":550,"headerVariant":562,"image":563,"keywords":564,"meta":565,"navigation":381,"ogDescription":568,"ogTitle":563,"path":569,"readTime":570,"schemaOrg":571,"schemaType":572,"seo":573,"sitemap":574,"stem":575,"tags":576,"twitterCard":577,"__hash__":578},"blog/blog/vulnerabilities/better-auth-pre-account-hijacking.md","Better Auth Account Takeover: Check Your Version (2026)",{"type":7,"value":8,"toc":534},"minimark",[9,18,21,37,42,45,130,141,150,154,157,199,208,211,215,218,221,236,239,248,252,259,266,286,299,318,327,331,337,340,408,418,430,433,437,440,447,461,501,518,530],[10,11,12,13,17],"p",{},"On June 26, 2026, Better Auth published a batch of security advisories. Two of them describe the same underlying trick, and it's one worth understanding even if you don't use Better Auth: an attacker signs up with your user's email address ",[14,15,16],"em",{},"before"," your user does, then keeps access after your user arrives and signs in.",[10,19,20],{},"Better Auth is a large project. The npm registry reports 6,610,694 downloads in the week of July 27 to August 2, 2026, and the GitHub repository sits at 29,451 stars. So this is not an obscure library, and if an AI tool scaffolded auth into your TypeScript app there is a reasonable chance this is what it reached for.",[22,23,24],"tldr",{},[10,25,26,27,31,32,36],{},"Two advisories, same bug class. An attacker registers your user's email with a password they choose. The account sits unverified. When your real user later signs in with a magic link, an email OTP, or a Google button, the older versions attach that sign-in to the attacker's row without removing the attacker's password. Both parties now have access. Upgrade to ",[28,29,30],"strong",{},"1.6.22 or later"," and you clear both. Then check your ",[33,34,35],"code",{},"@better-auth/*"," plugins separately, because they ship their own versions and their own advisories.",[38,39,41],"h2",{"id":40},"what-the-two-advisories-say","What the two advisories say",[10,43,44],{},"Both are scored 8.3 High by GitHub as the assigning authority. That's worth stating precisely: for CVE-2026-53516, the National Vulnerability Database has not published its own independent assessment, so 8.3 is GitHub's number rather than a second opinion.",[46,47,48,63],"table",{},[49,50,51],"thead",{},[52,53,54,57,60],"tr",{},[55,56],"th",{},[55,58,59],{},"Magic link and email OTP",[55,61,62],{},"OAuth",[64,65,66,78,89,100,119],"tbody",{},[52,67,68,72,75],{},[69,70,71],"td",{},"Advisory",[69,73,74],{},"GHSA-qq9h-g4jm-xgf3",[69,76,77],{},"GHSA-g38m-r43w-p2q7",[52,79,80,83,86],{},[69,81,82],{},"CVE",[69,84,85],{},"CVE-2026-67327",[69,87,88],{},"CVE-2026-53516",[52,90,91,94,97],{},[69,92,93],{},"Published",[69,95,96],{},"June 26, 2026",[69,98,99],{},"May 31, 2026",[52,101,102,105,114],{},[69,103,104],{},"Affected",[69,106,107,110,111],{},[33,108,109],{},">=1.1.3 \u003C1.6.22",", and ",[33,112,113],{},">=1.7.0-beta.0 \u003C1.7.0-beta.10",[69,115,116],{},[33,117,118],{},"\u003C1.6.11",[52,120,121,124,127],{},[69,122,123],{},"Fixed in",[69,125,126],{},"1.6.22, 1.7.0-beta.10",[69,128,129],{},"1.6.11",[10,131,132,133,136,137,140],{},"Note that the affected range for the first one is two disjoint intervals. If you're on a ",[33,134,135],{},"1.7.0-beta"," build, comparing your version against ",[33,138,139],{},"1.6.22"," with a single greater-than check gives you the wrong answer.",[142,143,144],"warning-box",{},[10,145,146,149],{},[28,147,148],{},"These are two advisories with two different fixes, not one bug filed twice."," The magic-link fix removes the attacker's password and revokes existing sessions before marking the address verified. The OAuth fix takes a different route: it refuses to link the OAuth identity at all when the local account was never verified. Same class, same root idea, separately patched.",[38,151,153],{"id":152},"the-attack-in-the-order-it-happens","The attack, in the order it happens",[10,155,156],{},"The thing that makes pre-account hijacking hard to spot is that step one happens long before anything looks wrong, and it happens to an account that isn't yours yet.",[158,159,160,175,181,187,193],"ol",{},[161,162,163,166,167,170,171,174],"li",{},[28,164,165],{},"The attacker signs up as your user."," They go to your sign-up form and register ",[33,168,169],{},"victim@company.com",", an address they do not own, with a password they choose. Your app sends a verification email to the real owner, who ignores it or never sees it. The row now exists with ",[33,172,173],{},"emailVerified: false",".",[161,176,177,180],{},[28,178,179],{},"Nothing happens for a while."," The attacker can't log in, because the address is unverified. The account is inert. This is the part that defeats monitoring: there is no failed-login spike, no anomaly, nothing to alert on.",[161,182,183,186],{},[28,184,185],{},"Your real user shows up."," They sign up or sign in the way most people actually do now, by clicking a magic link, entering an email OTP, or pressing \"Continue with Google\".",[161,188,189,192],{},[28,190,191],{},"The two identities merge."," That sign-in path finds an existing row for the address and attaches to it. The address is now verified, legitimately, because the real owner just proved they control the mailbox.",[161,194,195,198],{},[28,196,197],{},"The attacker's password is still on the account."," Nobody removed it. The attacker signs in with email and password whenever they like, and to your app they are the verified user.",[200,201,202],"danger-box",{},[10,203,204,207],{},[28,205,206],{},"Your user did everything right."," They received a real email at their own address, clicked a real link, and got a real session. There is no phishing here and nothing for them to have noticed. The failure is entirely in what the library did with the row that already existed.",[10,209,210],{},"Read step 5 against step 1 and the shape of it generalizes past this one library. Proving you control an email address today says nothing about who set the password on that account yesterday. Any system that lets an unverified account be created by one person and later claimed by another through a different door has to decide what happens to the first person's credentials, and \"leave them attached\" is the wrong answer.",[38,212,214],{"id":213},"whether-youre-actually-exploitable","Whether you're actually exploitable",[10,216,217],{},"An unpatched version is not automatically a live incident, and it's worth being precise rather than alarming.",[10,219,220],{},"The magic-link advisory lists three conditions, and it says all of them must hold:",[222,223,224,227,230],"ul",{},[161,225,226],{},"You're running an affected version.",[161,228,229],{},"You have the magic-link plugin or the email-OTP plugin enabled.",[161,231,232,233,174],{},"You allow ",[28,234,235],{},"open registration with email and password sign-up",[10,237,238],{},"That third one is the interesting condition. If your app only offers magic links and has no password sign-up at all, there is no way for an attacker to plant a password on a pre-registered row, and this specific advisory doesn't apply to you.",[240,241,242],"info-box",{},[10,243,244,247],{},[28,245,246],{},"Most vibe-coded apps meet all three conditions anyway."," The common pattern is to offer several ways in at once, because each one is a checkbox in the auth config and adding another feels free. Email and password, plus Google, plus a magic link. Each door is fine. It's the combination, plus open registration, that creates the hallway between them.",[38,249,251],{"id":250},"the-check-you-can-run-in-five-minutes","The check you can run in five minutes",[10,253,254,255,258],{},"Version comparison tells you whether you ",[14,256,257],{},"could"," be affected. This tells you whether the behavior is actually present in your running app, which is a different and better question.",[10,260,261,262,265],{},"You need two email addresses you control. A plus-address like ",[33,263,264],{},"you+attacker@gmail.com"," works, since most providers deliver it to the same inbox while your app treats it as distinct.",[267,268,270,275],"step",{"number":269},"1",[271,272,274],"h3",{"id":273},"register-the-address-without-verifying-it","Register the address without verifying it",[10,276,277,278,281,282,285],{},"On your live app, sign up with email and password using ",[33,279,280],{},"you+victim@gmail.com",". Pick a password and write it down. Do ",[28,283,284],{},"not"," click the verification email. This is the attacker's move.",[267,287,289,293],{"number":288},"2",[271,290,292],{"id":291},"now-claim-the-same-address-the-other-way","Now claim the same address the other way",[10,294,295,296,298],{},"In a private browsing window, sign in with a magic link (or Google, if that address is a Google account) using the same ",[33,297,280],{},". Complete it normally. This is your real user's move.",[267,300,302,306,312],{"number":301},"3",[271,303,305],{"id":304},"go-back-and-try-the-password","Go back and try the password",[10,307,308,309,311],{},"Sign out. Now sign in with ",[33,310,280],{}," and the password from step 1.",[10,313,314,317],{},[28,315,316],{},"If that password still works, you have the bug."," Two separate acts of signing up produced one account with two credentials, and only one of them ever proved control of the mailbox.",[319,320,321],"tip-box",{},[10,322,323,326],{},[28,324,325],{},"Do this on a staging copy if you can, and delete the test account afterward either way."," You're deliberately creating an account in a state your app isn't expecting, and leaving it behind in production is untidy at best.",[38,328,330],{"id":329},"fixing-it","Fixing it",[10,332,333,334,336],{},"Upgrade the core package to ",[28,335,30],{},". That clears both advisories, since 1.6.22 is above the 1.6.11 fix line for the OAuth one. On the beta line, 1.7.0-beta.10.",[10,338,339],{},"Then do the part people skip:",[341,342,344],"code-block",{"label":343},"Check the plugins too, not just the core",[345,346,351],"pre",{"className":347,"code":348,"language":349,"meta":350,"style":350},"language-bash shiki shiki-themes github-dark","# The core package\nnpm ls better-auth\n\n# The plugins, which version and patch independently\nnpm ls @better-auth/stripe @better-auth/sso @better-auth/scim @better-auth/oauth-provider\n","bash","",[33,352,353,362,376,383,389],{"__ignoreMap":350},[354,355,358],"span",{"class":356,"line":357},"line",1,[354,359,361],{"class":360},"sAwPA","# The core package\n",[354,363,365,369,373],{"class":356,"line":364},2,[354,366,368],{"class":367},"svObZ","npm",[354,370,372],{"class":371},"sU2Wk"," ls",[354,374,375],{"class":371}," better-auth\n",[354,377,379],{"class":356,"line":378},3,[354,380,382],{"emptyLinePlaceholder":381},true,"\n",[354,384,386],{"class":356,"line":385},4,[354,387,388],{"class":360},"# The plugins, which version and patch independently\n",[354,390,392,394,396,399,402,405],{"class":356,"line":391},5,[354,393,368],{"class":367},[354,395,372],{"class":371},[354,397,398],{"class":371}," @better-auth/stripe",[354,400,401],{"class":371}," @better-auth/sso",[354,403,404],{"class":371}," @better-auth/scim",[354,406,407],{"class":371}," @better-auth/oauth-provider\n",[10,409,410,411,414,415,174],{},"That second command matters because the June 26 batch was not two advisories. It was five, and several were in plugin packages with their own version ranges. ",[33,412,413],{},"@better-auth/stripe"," had a cross-organization billing flaw (GHSA-h3rm-78g3-j7cp, CVE-2026-67329, fixed in 1.6.21) where the permission check read the organization ID from the query string while the handler acted on the one in the request body. The most severe of the batch, rated Critical, was in ",[33,416,417],{},"@better-auth/scim",[240,419,420],{},[10,421,422,425,426,429],{},[28,423,424],{},"The SCIM and SSO advisories probably don't apply to you",", and it would be misleading to imply otherwise. Those plugins serve enterprise identity setups, not a founder's first product. They're worth knowing about only because ",[33,427,428],{},"npm ls better-auth"," won't mention them, so if you did install one, checking the core version will tell you nothing about it.",[10,431,432],{},"After upgrading, one more thing that the version bump does not do for you: the fix changes behavior going forward, but any account already sitting in the merged state stays merged. If your app has been live on an affected version with open registration, consider querying for accounts that have both a password credential and a linked OAuth identity where the email verification came later than the password was set. What to do about them is a judgment call. Forcing a password reset on that set is the conservative option.",[38,434,436],{"id":435},"why-this-class-of-bug-keeps-appearing","Why this class of bug keeps appearing",[10,438,439],{},"It's tempting to read this as one library being careless. The more useful reading is that email verification carries a meaning most code quietly gets wrong.",[10,441,442,443,446],{},"Verifying an address proves that whoever clicked the link controls that mailbox ",[14,444,445],{},"right now",". It says nothing about who created the row, when, or what they attached to it. Treating verification as a blessing on everything already on the account is the mistake, and it's an easy one, because in the common case the person who signed up and the person who verified are obviously the same human.",[10,448,449,450,455,456,460],{},"The fix in both advisories is the same principle applied two ways: current proof of control wins, and anything attached before that proof is not trusted. That's a good rule to hold onto whichever auth library you end up on. If you're weighing that decision, our ",[451,452,454],"a",{"href":453},"/blog/vulnerabilities/next-auth-v5-fail-open","NextAuth v5 fail-open"," writeup covers a different failure in the same layer, and the ",[451,457,459],{"href":458},"/blog/how-to/magic-links","magic link guide"," covers what a correct implementation has to do with single-use tokens.",[462,463,464,471,477,489,495],"faq-section",{},[465,466,468],"faq-item",{"question":467},"Which Better Auth versions are affected?",[10,469,470],{},"For the magic-link and email-OTP advisory (GHSA-qq9h-g4jm-xgf3, CVE-2026-67327), versions 1.1.3 up to but not including 1.6.22 on the stable line, plus 1.7.0-beta.0 through 1.7.0-beta.9. Fixed in 1.6.22 and 1.7.0-beta.10. For the OAuth advisory (CVE-2026-53516), everything before 1.6.11, fixed in 1.6.11. Upgrading to 1.6.22 or later covers both.",[465,472,474],{"question":473},"Am I actually exploitable, or just on an old version?",[10,475,476],{},"The magic-link advisory states three conditions, all of which must hold: you run an affected version, you have the magic-link or email-OTP plugin enabled, and you allow open registration with email and password sign-up. An app that only offers magic links, with no password sign-up at all, isn't exploitable by that specific advisory. Upgrade anyway, but this is why an unpatched version is not automatically a live incident.",[465,478,480],{"question":479},"How do I check what version I am on?",[10,481,482,483,485,486,488],{},"Run ",[33,484,428],{}," for the core package. That isn't the whole answer though, because the Better Auth plugins ship as separate npm packages with their own version ranges and their own advisories. If you use any ",[33,487,35],{}," package, check each one against its own advisory rather than assuming the core version covers it.",[465,490,492],{"question":491},"What is pre-account hijacking?",[10,493,494],{},"The attacker creates an account using an email address they don't control, before the real owner ever signs up. The account sits unverified, so the attacker can't use it yet. Later the real owner arrives and signs in through a different door, such as a magic link or a Google button. If that path attaches to the attacker's existing row without clearing out what the attacker planted, both people now have access to one account.",[465,496,498],{"question":497},"Does this affect NextAuth or Clerk too?",[10,499,500],{},"These specific advisories are Better Auth's. The underlying pattern isn't: any system where an unverified account can be created by one person and later claimed by another through a different sign-in path has to decide what happens to the first person's credentials. That's a design question every auth library answers, and the answer is worth knowing for whichever one you use.",[502,503,504,509,513],"related-articles",{},[505,506],"related-card",{"description":507,"href":453,"title":508},"A different failure in the same layer: a config error that let requests through instead of stopping them","NextAuth v5 Fail-Open",[505,510],{"description":511,"href":458,"title":512},"What a correct passwordless flow has to do with single-use tokens, expiry, and the account it lands on","How to Implement Magic Links Securely",[505,514],{"description":515,"href":516,"title":517},"Other ways a request ends up authenticated as somebody it should not be","/blog/vulnerabilities/api-authentication-bypass","API Authentication Bypass",[519,520,523,527],"cta-box",{"href":521,"label":522},"/","Start Free Scan",[38,524,526],{"id":525},"not-sure-what-your-apps-auth-actually-does","Not sure what your app's auth actually does?",[10,528,529],{},"A scan checks your deployed app from the outside for exposed endpoints, misconfigured CORS, leaked keys and missing headers. No signup needed for your first results.",[531,532,533],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":350,"searchDepth":364,"depth":364,"links":535},[536,537,538,539,544,545,546],{"id":40,"depth":364,"text":41},{"id":152,"depth":364,"text":153},{"id":213,"depth":364,"text":214},{"id":250,"depth":364,"text":251,"children":540},[541,542,543],{"id":273,"depth":378,"text":274},{"id":291,"depth":378,"text":292},{"id":304,"depth":378,"text":305},{"id":329,"depth":364,"text":330},{"id":435,"depth":364,"text":436},{"id":525,"depth":364,"text":526},"vulnerabilities","2026-08-04","Two Better Auth advisories let an attacker pre-register your user's email and keep password access after that user signs in. Which versions are affected, and the test you can run yourself.",false,"md",[553,554,556,558,560],{"question":467,"answer":470},{"question":473,"answer":555},"The magic-link advisory states three conditions, all of which must hold: you run an affected version, you have the magic-link or email-OTP plugin enabled, and you allow open registration with email and password sign-up. An app that only offers magic links, with no password sign-up at all, is not exploitable by that specific advisory. Upgrade anyway, but this is why an unpatched version is not automatically a live incident.",{"question":479,"answer":557},"Run npm ls better-auth for the core package. That is not the whole answer though, because the Better Auth plugins ship as separate npm packages with their own version ranges and their own advisories. If you use any @better-auth/* package, check each one against its own advisory rather than assuming the core version covers it.",{"question":491,"answer":559},"The attacker creates an account using an email address they do not control, before the real owner ever signs up. The account sits unverified, so the attacker cannot use it yet. Later the real owner arrives and signs in through a different door, such as a magic link or a Google button. If that path attaches to the attacker's existing row without clearing out what the attacker planted, both people now have access to one account.",{"question":497,"answer":561},"These specific advisories are Better Auth's. The underlying pattern is not: any system where an unverified account can be created by one person and later claimed by another through a different sign-in path has to decide what happens to the first person's credentials. That is a design question every auth library answers, and the answer is worth knowing for whichever one you use.","red",null,"better auth account takeover, better-auth vulnerability, CVE-2026-67327, CVE-2026-53516, pre-account hijacking, better auth magic link security, is better-auth safe",{"trendTrigger":566,"trendDate":567},"better-auth-advisory-batch-pre-account-hijacking","2026-06-26","An attacker signs up with your user's email before they do. When your user later signs in with a magic link, the attacker's password still works.","/blog/vulnerabilities/better-auth-pre-account-hijacking","9 min read","[object Object]","Article",{"title":5,"description":549},{"loc":569},"blog/vulnerabilities/better-auth-pre-account-hijacking",[],"summary_large_image","qwPoNTd1QNrYWd6_c_AQEwnD3R-9aNTFOjxEfr709xQ",1785862008453]