{"id":"GHSA-r4xp-prcw-77qf","kind":"vulnerability","aliases":["GHSA-r4xp-prcw-77qf"],"related":[],"title":"better-auth: OAuth Proxy lets an attacker sign in as another user","description":"## Am I affected?\n\nYou may be affected if you use `better-auth` from 1.5.0-beta.12 through versions before 1.7.7 with the OAuth Proxy plugin. You are affected only if **all** of these are also true:\n\n- On a participating environment, the OAuth Proxy plugin has no dedicated `secret`, or its `secret` equals that environment's global Better Auth secret.\n- Sign-in state is stored in a cookie (`account.storeStateStrategy: \"cookie\"`). With a database or secondary storage, this is only the case if you chose it explicitly.\n- Implicit account linking is on (the default).\n\nYou are not affected by this path if every participating environment uses a proxy `secret` distinct from its global Better Auth secret, if sign-in state is stored in the database, or if you use a version before 1.5.0-beta.12.\n\n## Summary\n\nThe OAuth Proxy plugin trusts any user profile it can decrypt. In the affected setup, Better Auth encrypts sign-in state, including data the caller chose, with the same secret and hands it to anyone who starts a social sign-in. An attacker can use this to get a fake profile for another user's email address that the proxy accepts as real.\n\n## Impact\n\nAn attacker without an account who knows a user's verified email address can sign in as that user, with that user's permissions. They can read private data, change the account, and link a provider account of their choice to it. They need no provider sign-in and no action from the victim. No availability impact is established, and a reported open redirect was not confirmed.\n\n## Patches\n\nThe fix is merged for the planned `better-auth` 1.7.7 release. That package is not available yet.\n\nUpgrade all nodes handling the same cookie-backed OAuth or SAML state flow and all OAuth Proxy participants together, including the production coordinator and preview or development deployments. Keep installed Better Auth packages released alongside `better-auth` on the same release version. Restart pending sign-in and account-linking flows; pre-upgrade state cookies and proxy payloads are rejected. Public options and API signatures are unchanged.\n\n## Workarounds\n\nUntil you upgrade to the fixed release, apply one of these:\n\n- Set one strong OAuth Proxy `secret` across every environment that takes part in the proxy flow, and keep it different from each environment's global Better Auth secret.\n- Set `account.storeStateStrategy: \"database\"`. This is already the default when a database or secondary storage is configured. If Magic Link is also enabled, switching can expose a separate cross-purpose verification-record path in affected releases. Ensure the two flows resolve to distinct stored identifiers; with built-in options, use Magic Link `storeToken: \"hashed\"` with global `verification.storeIdentifier` unset or `\"plain\"`, or disable Magic Link or both built-in social and Generic OAuth sign-in.\n- Remove the OAuth Proxy plugin if you do not need it.\n\nSetting `account.accountLinking.disableImplicitLinking: true` stops this path from taking over an existing account. It does not fix the underlying flaw: the callback can still accept attacker-made data as a provider identity. Apply one of the steps above as well.","summary":"The OAuth Proxy plugin in affected better-auth releases trusts any user profile it can decrypt, and when the proxy secret matches the global Better Auth secret and sign-in state is stored in cookies, anyone can build a fake profile for a victim's email. The attacker then signs in as that user without an account or victim action.","severity":"high","cvss":7.4,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"better-auth","fixed":["1.7.7"],"affected":">= 1.5.0-beta.12, < 1.7.7","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["better-auth","cwe-287","cwe-345","node","oauth-proxy","package","self-hosted"],"not_affected_if":["database-state-storage","default-config","distinct-proxy-secret","pre-1.5.0-beta.12"],"how_to_check":"- Run `npm ls better-auth` and confirm the installed version is below `1.7.7` and at least `1.5.0-beta.12`.\n- Check whether the OAuth Proxy plugin is configured and whether its `secret` is unset or equal to the global Better Auth secret.\n- Check whether `account.storeStateStrategy` is set to `\"cookie\"`, and whether implicit account linking is enabled (the default).","fix":"- Upgrade `better-auth` to `1.7.7` once released, across every node and environment in the proxy flow, including preview and development deployments.\n- Until then, set a distinct strong OAuth Proxy `secret` in every participating environment, or set `account.storeStateStrategy: \"database\"`, or remove the OAuth Proxy plugin.\n- Restart pending sign-in and account-linking flows after upgrading, since pre-upgrade state cookies are rejected.","status":"open","published_at":"2026-10-27T15:32:16+00:00","first_seen_at":"2026-10-27T15:34:04+00:00","triaged_at":"2026-10-27T15:35:43+00:00","updated_at":"2026-10-27T15:35:43+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-27T15:35:43Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade `better-auth` to `1.7.7` once released, across every node and environment in the proxy flow, including preview and development deployments.\n- Until then, set a distinct strong OAuth Proxy `secret` in every participating environment, or set `account.storeStateStrategy: \"database\"`, or remove the OAuth Proxy plugin.\n- Restart pending sign-in and account-linking flows after upgrading, since pre-upgrade state cookies are rejected.","reach":"internet","title":"better-auth: OAuth Proxy lets an attacker sign in as another user","impact":"high","summary":"The OAuth Proxy plugin in affected better-auth releases trusts any user profile it can decrypt, and when the proxy secret matches the global Better Auth secret and sign-in state is stored in cookies, anyone can build a fake profile for a victim's email. The attacker then signs in as that user without an account or victim action.","category":"package","nicknames":[],"who_cares":["better-auth","node","self-hosted","oauth-proxy"],"confidence":0.8,"needs_auth":"no","worst_case":"Attacker signs in as any user whose verified email they know","analyst_note":"","how_to_check":"- Run `npm ls better-auth` and confirm the installed version is below `1.7.7` and at least `1.5.0-beta.12`.\n- Check whether the OAuth Proxy plugin is configured and whether its `secret` is unset or equal to the global Better Auth secret.\n- Check whether `account.storeStateStrategy` is set to `\"cookie\"`, and whether implicit account linking is enabled (the default).","needs_analyst":false,"not_affected_if":["default-config","database-state-storage","distinct-proxy-secret","pre-1.5.0-beta.12"],"who_is_affected":"Applications running `better-auth` from `1.5.0-beta.12` up to but not including `1.7.7` that use the OAuth Proxy plugin with cookie-backed sign-in state and implicit account linking. The exposure depends on the proxy secret matching the global secret across environments, so teams using database or secondary storage for state, or a distinct proxy secret, are not exposed by this path.","user_interaction":"no","needs_special_setup":"yes","severity_if_unknown":"high"},"cost_usd":0.000823,"input_tokens":4306,"output_tokens":784,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-287':31B '-345':33B '1.5.0':118C,236C '1.7.7':123C,384C '77qf':18A,23A 'accept':301C,615C 'account':100B,191C,309C,336C,341C,443C,603C 'account-link':442C 'account.accountlinking.disableimplicitlinking':593C 'account.storestatestrategy':172C,504C 'across':481C 'action':103B,356C 'address':297C,317C 'affect':47B,106C,110C,131C,200C,252C,538C 'alongsid':427C 'alreadi':508C 'also':138C,523C 'anoth':12A,293C 'anyon':79B,274C 'api':459C 'appli':471C,624C 'attack':8A,91B,283C,306C,617C 'attacker-mad':616C 'auth':3A,26A,29B,50B,68B,116C,161C,217C,255C,383C,424C,430C,501C 'avail':361C,390C 'back':400C 'beta.12':119C,237C 'better':2A,25A,28B,49B,67B,115C,160C,216C,254C,382C,423C,429C,500C 'better-auth':1A,24A,27B,48B,114C,381C,428C 'build':81B 'built':551C,572C 'built-in':550C,571C 'callback':612C 'caller':264C 'case':184C 'chang':334C 'choic':344C 'chose':187C,265C 'configur':518C 'confirm':372C 'cooki':78B,171C,173C,399C,450C 'cookie-back':398C 'coordin':415C 'cross':531C 'cross-purpos':530C 'cwe':30B,32B 'data':262C,333C,619C 'databas':176C,228C,505C,513C 'decrypt':58B,249C 'dedic':150C 'default':196C,510C 'deploy':420C 'develop':419C 'differ':494C 'disabl':566C 'distinct':212C,546C 'email':89B,296C,316C 'enabl':524C 'encrypt':256C 'ensur':540C 'environ':143C,157C,207C,483C,497C 'equal':155C 'establish':364C 'everi':205C,482C 'exist':602C 'explicit':189C 'expos':527C 'fake':83B,290C 'fix':375C,469C,607C 'flaw':610C 'flow':405C,445C,490C,543C 'generic':576C 'get':288C 'ghsa':15A,20A 'ghsa-r4xp-prcw-77qf':14A,19A 'global':66B,159C,215C,499C,560C 'hand':271C 'handl':395C 'hash':558C 'host':41B 'ident':623C 'identifi':548C 'impact':304C,362C 'implicit':190C 'includ':261C,412C 'instal':422C 'keep':421C,492C 'know':311C 'let':6A 'link':192C,338C,444C,521C,556C,568C 'made':618C 'magic':520C,555C,567C 'match':64B 'may':108C 'merg':377C 'need':348C,590C 'node':34B,394C 'oauth':4A,36B,43B,126C,145C,240C,401C,408C,478C,577C,583C 'oauth-proxi':35B 'one':472C,476C,625C 'open':368C 'option':457C,553C 'packag':38B,387C,425C 'part':486C 'particip':142C,206C,410C 'patch':373C 'path':203C,536C,597C 'payload':453C 'pend':437C 'permiss':328C 'plain':564C 'plan':380C 'plugin':45B,128C,147C,242C,585C 'prcw':17A,22A 'pre':447C 'pre-upgrad':446C 'preview':417C 'privat':332C 'product':414C 'profil':55B,84B,246C,291C 'provid':340C,350C,622C 'proxi':5A,37B,44B,62B,127C,146C,210C,241C,300C,409C,452C,479C,489C,584C 'public':456C 'purpos':532C 'r4xp':16A,21A 'read':331C 'real':303C 'record':535C 'redirect':369C 'reject':455C 'releas':51B,385C,426C,434C,470C,539C 'remov':581C 'report':367C 'resolv':544C 'restart':436C 'saml':403C 'secondari':178C,515C 'secret':63B,69B,151C,154C,162C,211C,218C,269C,480C,502C 'self':40B 'self-host':39B 'separ':529C 'set':475C,503C,592C 'setup':253C 'sign':9A,72B,93B,164C,221C,258C,280C,319C,352C,439C,579C 'sign-in':71B,163C,220C,257C,279C,351C,438C,578C 'signatur':460C 'social':278C,574C 'start':276C 'state':74B,166C,223C,260C,404C,449C 'step':628C 'still':614C 'stop':595C 'storag':179C,516C 'store':76B,168C,225C,547C 'storetoken':557C 'strong':477C 'summari':238C 'switch':525C 'take':485C,599C 'togeth':411C 'true':139C,594C 'trust':52B,243C 'two':542C 'unchang':462C 'under':609C 'unset':562C 'upgrad':392C,448C,466C 'use':113C,208C,232C,285C,554C 'user':13A,54B,97B,245C,294C,313C,323C,326C 'verif':534C 'verifi':315C 'verification-record':533C 'verification.storeidentifier':561C 'version':121C,234C,435C 'victim':87B,102B,359C 'well':631C 'without':98B,307C 'workaround':463C 'yet':391C","nicknames":[],"mitigations":[],"who_affected":"Applications running `better-auth` from `1.5.0-beta.12` up to but not including `1.7.7` that use the OAuth Proxy plugin with cookie-backed sign-in state and implicit account linking. The exposure depends on the proxy secret matching the global secret across environments, so teams using database or secondary storage for state, or a distinct proxy secret, are not exposed by this path.","tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-27T15:32:16+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-r4xp-prcw-77qf","url":"https://github.com/advisories/GHSA-r4xp-prcw-77qf","severity":"high","published_at":"2026-10-27T15:32:16+00:00","modified_at":"2026-10-27T15:32:17+00:00","first_seen_at":"2026-10-27T15:34:04+00:00","fetched_at":"2026-10-27T15:34:04+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}