{"id":"GHSA-4wq4-2ppv-q3hg","kind":"vulnerability","aliases":["GHSA-4wq4-2ppv-q3hg"],"related":[],"title":"@better-auth/expo: Authorization proxy can sign values it did not issue","description":"## Am I affected?\n\nYou are affected if your server registers the `expo()` plugin from `@better-auth/expo` 1.4.0-beta.6 or later. No other setting is needed.\n\nThe wildcard origin problem below also needs a wildcard entry in `trustedOrigins`, such as `myapp://*` or `https://*.example.com`.\n\n## Summary\n\n**Signing values it did not issue.** The `expo()` plugin adds a public endpoint that helps the system browser start a social sign-in. The endpoint signed a value taken from the request with the application secret and returned the signature in a cookie. Better Auth uses the same secret for email verification links and, when enabled, the session cookie cache.\n\n**Open redirect.** The same endpoint could redirect a visitor to any external HTTPS site.\n\n**Wildcard trusted origins.** A wildcard entry in `trustedOrigins` could match URLs outside the intended scheme, host, or path.\n\n## Impact\n\nAn unauthenticated attacker who knows a user's email address can sign in as that user and change the account's email address, with no action from the user. With the session cookie cache enabled, an attacker who knows a user's ID can also act as that user.\n\nThe open redirect can make phishing links that start on your auth origin look more convincing. The wildcard problem can let a URL you did not mean to trust pass the trusted origin check.\n\n## Patches\n\nUpgrade both `@better-auth/expo` and `better-auth` to 1.7.16, or to 1.8.0-beta.5 on the beta line.\n\nAfter upgrading, rotate the application secret if the endpoint was reachable. Signatures issued before the fix stay valid until the secret changes.\n\nIf Expo sign-in through an SSO identity provider fails after upgrading, add the identity provider's origin to `trustedOrigins`.\n\n## Workarounds\n\nIf you do not use Expo social sign-in, remove the `expo()` server plugin.\n\nOtherwise, block requests to `/expo-authorization-proxy` (under your auth base path) whose `authorizationURL` carries a `state` value that is not exactly 32 characters from `A-Z`, `a-z`, `0-9`, `-` and `_`. State values that Better Auth issues always have this form. This blocks the signing problem only.\n\n## Technical details\n\nThe endpoint read `state` from the caller-supplied authorization URL and stored it in a signed cookie without checking that the server had issued it. The cookie signature is an HMAC-SHA256 keyed with the application secret. That is the same computation as an HS256 JSON Web Token signature and as the compact session cookie cache signature, so a forged email-change token or cookie cache value was accepted.\n\nThe fix signs only an unexpired state the server issued, and redirects only to the authorization endpoint origin of a configured social, generic OAuth, or trusted SSO provider. Wildcard trusted origins now match a URL by its parsed scheme, host, and path, and no longer match a URL with userinfo or one that cannot be parsed. A wildcard host made only of digits, dots, and wildcards, such as `192.168.*.*`, now matches only IPv4 addresses.","summary":"The expo() plugin in @better-auth/expo signed a caller-supplied state value with the application secret and returned it in a cookie. An unauthenticated attacker who knows a user's email can sign in as that user and change the account's email address.","severity":"critical","cvss":9.1,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"@better-auth/expo","fixed":["1.7.16"],"affected":">= 1.4.0-beta.6, < 1.7.16","ecosystem":"npm"},{"name":"@better-auth/expo","fixed":["1.8.0-beta.5"],"affected":">= 1.8.0-beta.0, < 1.8.0-beta.5","ecosystem":"npm"},{"name":"better-auth","fixed":["1.7.16"],"affected":"< 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":["auth","better-auth","cwe-287","cwe-345","cwe-601","expo","node","package"],"not_affected_if":["no-expo-plugin","versions-patched"],"how_to_check":"- Check whether the server registers the `expo()` plugin from `@better-auth/expo` at version `1.4.0-beta.6` or later.\n- Check installed versions of `@better-auth/expo` and `better-auth` with `npm ls @better-auth/expo better-auth`.\n- Check whether `trustedOrigins` contains a wildcard entry such as `myapp://*` or `https://*.example.com`.","fix":"- `@better-auth/expo` to `1.7.16` on the 1.7 line, or `1.8.0-beta.5` on the beta line.\n- `better-auth` to `1.7.16` on the 1.7 line, or `1.8.0-beta.5` on the beta line.\n- Rotate the application secret after upgrading if the endpoint was reachable.","status":"open","published_at":"2026-11-03T21:37:25+00:00","first_seen_at":"2026-11-09T22:53:20+00:00","triaged_at":"2026-11-09T22:54:05+00:00","updated_at":"2026-11-09T22:54:05+00:00","category":"package","confidence":0.9,"triage":{"at":"2026-11-09T22:54:05Z","model":"claude-haiku-5-5","output":{"fix":"- `@better-auth/expo` to `1.7.16` on the 1.7 line, or `1.8.0-beta.5` on the beta line.\n- `better-auth` to `1.7.16` on the 1.7 line, or `1.8.0-beta.5` on the beta line.\n- Rotate the application secret after upgrading if the endpoint was reachable.","reach":"internet","title":"Better Auth Expo plugin lets attackers forge signed values and sign in","impact":"high","summary":"The expo() plugin in @better-auth/expo signed a caller-supplied state value with the application secret and returned it in a cookie. An unauthenticated attacker who knows a user's email can sign in as that user and change the account's email address.","audience":"developers","category":"package","nicknames":[],"who_cares":["better-auth","expo","node","auth"],"confidence":0.9,"fix_status":"available","needs_auth":"no","worst_case":"Attacker signs in as any user with their email and changes it.","analyst_note":"","how_to_check":"- Check whether the server registers the `expo()` plugin from `@better-auth/expo` at version `1.4.0-beta.6` or later.\n- Check installed versions of `@better-auth/expo` and `better-auth` with `npm ls @better-auth/expo better-auth`.\n- Check whether `trustedOrigins` contains a wildcard entry such as `myapp://*` or `https://*.example.com`.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["no-expo-plugin","versions-patched"],"who_is_affected":"Applications whose server registers the expo() plugin from @better-auth/expo 1.4.0-beta.6 or later are exposed, with no other setting required. The wildcard open-redirect and trusted-origin issues additionally need a wildcard entry in trustedOrigins. Servers that do not use Expo social sign-in can remove the expo() plugin instead.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"critical"},"cost_usd":0.000928,"input_tokens":5712,"output_tokens":713,"severity_before":"critical"},"analyst_status":null,"analyst_note":null,"search":"'-287':43B '-345':45B '-601':47B '-9':429C '/expo':4A,27A,31A,58B,115C,325C '/expo-authorization-proxy':403C '0':428C '1.4.0':116C '1.7.16':331C '1.8.0':334C '192.168':589C '2ppv':17A,22A '32':419C '4wq4':16A,21A 'a-z':422C,425C 'accept':520C 'account':94B,255C 'act':281C 'action':261C 'add':151C,375C 'address':97B,245C,258C,594C 'affect':100C,103C 'also':130C,280C 'alway':437C 'applic':68B,177C,344C,486C 'attack':78B,238C,272C 'auth':3A,26A,30A,34A,37A,38B,41B,57B,114C,187C,296C,324C,329C,406C,435C 'author':5A,458C,536C 'authorizationurl':410C 'base':407C 'beta':338C 'beta.5':335C 'beta.6':117C 'better':2A,25A,29A,33A,36A,40B,56B,113C,186C,323C,328C,434C 'better-auth':1A,24A,28A,32A,35A,39B,55B,112C,322C,327C 'block':400C,442C 'browser':159C 'cach':202C,269C,506C,517C 'caller':62B,456C 'caller-suppli':61B,455C 'cannot':574C 'carri':411C 'chang':92B,253C,361C,513C 'charact':420C 'check':318C,468C 'compact':503C 'comput':492C 'configur':541C 'convinc':300C 'cooki':75B,185C,201C,268C,466C,476C,505C,516C 'could':208C,225C 'cwe':42B,44B,46B 'detail':448C 'digit':583C 'dot':584C 'email':84B,96B,193C,244C,257C,512C 'email-chang':511C 'enabl':198C,270C 'endpoint':154C,167C,207C,348C,450C,537C 'entri':134C,222C 'exact':418C 'example.com':140C 'expo':48B,52B,109C,149C,363C,389C,396C 'extern':214C 'fail':372C 'fix':355C,522C 'forg':510C 'form':440C 'generic':543C 'ghsa':15A,20A 'ghsa-4wq4-2ppv-q3hg':14A,19A 'help':156C 'hmac':481C 'hmac-sha256':480C 'host':232C,560C,579C 'hs256':495C 'https':215C 'id':278C 'ident':370C,377C 'impact':235C 'intend':230C 'ipv4':593C 'issu':13A,147C,352C,436C,473C,530C 'json':496C 'key':483C 'know':80B,240C,274C 'later':119C 'let':305C 'line':339C 'link':195C,291C 'longer':565C 'look':298C 'made':580C 'make':289C 'match':226C,553C,566C,591C 'mean':311C 'need':124C,131C 'node':49B 'oauth':544C 'one':572C 'open':203C,286C 'origin':127C,219C,297C,317C,380C,538C,551C 'otherwis':399C 'outsid':228C 'packag':50B 'pars':558C,576C 'pass':314C 'patch':319C 'path':234C,408C,562C 'phish':290C 'plugin':53B,110C,150C,398C 'problem':128C,303C,445C 'provid':371C,378C,548C 'proxi':6A 'public':153C 'q3hg':18A,23A 'reachabl':350C 'read':451C 'redirect':204C,209C,287C,532C 'regist':107C 'remov':394C 'request':174C,401C 'return':71B,180C 'rotat':342C 'scheme':231C,559C 'secret':69B,178C,191C,345C,360C,487C 'server':106C,397C,471C,529C 'session':200C,267C,504C 'set':122C 'sha256':482C 'sign':8A,59B,86B,142C,164C,168C,247C,365C,392C,444C,465C,523C 'sign-in':163C,364C,391C 'signatur':182C,351C,477C,499C,507C 'site':216C 'social':162C,390C,542C 'sso':369C,547C 'start':160C,293C 'state':64B,413C,431C,452C,527C 'stay':356C 'store':461C 'summari':141C 'suppli':63B,457C 'system':158C 'taken':171C 'technic':447C 'token':498C,514C 'trust':218C,313C,316C,546C,550C 'trustedorigin':136C,224C,382C 'unauthent':77B,237C 'unexpir':526C 'upgrad':320C,341C,374C 'url':227C,307C,459C,555C,568C 'use':188C,388C 'user':82B,90B,242C,251C,264C,276C,284C 'userinfo':570C 'valid':357C 'valu':9A,65B,143C,170C,414C,432C,518C 'verif':194C 'visitor':211C 'web':497C 'whose':409C 'wildcard':126C,133C,217C,221C,302C,549C,578C,586C 'without':467C 'workaround':383C 'z':424C,427C","nicknames":[],"mitigations":[],"who_affected":"Applications whose server registers the expo() plugin from @better-auth/expo 1.4.0-beta.6 or later are exposed, with no other setting required. The wildcard open-redirect and trusted-origin issues additionally need a wildcard entry in trustedOrigins. Servers that do not use Expo social sign-in can remove the expo() plugin instead.","tier":"B","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["betterauth","expo"],"ptokens":["auth","better","expo"],"labels":["auth","betterauth","critical","cwe287","cwe345","cwe601","expo","node","npm","package"],"campaign_type":null,"search_a":"'/expo':4A,27A,31A '2ppv':17A,22A '4wq4':16A,21A 'auth':3A,26A,30A,34A,37A 'author':5A 'better':2A,25A,29A,33A,36A 'better-auth':1A,24A,28A,32A,35A 'ghsa':15A,20A 'ghsa-4wq4-2ppv-q3hg':14A,19A 'issu':13A 'proxi':6A 'q3hg':18A,23A 'sign':8A 'valu':9A","search_b":"'-287':43B '-345':45B '-601':47B '/expo':58B 'account':94B 'address':97B 'applic':68B 'attack':78B 'auth':38B,41B,57B 'better':40B,56B 'better-auth':39B,55B 'caller':62B 'caller-suppli':61B 'chang':92B 'cooki':75B 'cwe':42B,44B,46B 'email':84B,96B 'expo':48B,52B 'know':80B 'node':49B 'packag':50B 'plugin':53B 'return':71B 'secret':69B 'sign':59B,86B 'state':64B 'suppli':63B 'unauthent':77B 'user':82B,90B 'valu':65B","search_c":"'-9':429C '/expo':115C,325C '/expo-authorization-proxy':403C '0':428C '1.4.0':116C '1.7.16':331C '1.8.0':334C '192.168':589C '32':419C 'a-z':422C,425C 'accept':520C 'account':255C 'act':281C 'action':261C 'add':151C,375C 'address':245C,258C,594C 'affect':100C,103C 'also':130C,280C 'alway':437C 'applic':177C,344C,486C 'attack':238C,272C 'auth':114C,187C,296C,324C,329C,406C,435C 'author':458C,536C 'authorizationurl':410C 'base':407C 'beta':338C 'beta.5':335C 'beta.6':117C 'better':113C,186C,323C,328C,434C 'better-auth':112C,322C,327C 'block':400C,442C 'browser':159C 'cach':202C,269C,506C,517C 'caller':456C 'caller-suppli':455C 'cannot':574C 'carri':411C 'chang':253C,361C,513C 'charact':420C 'check':318C,468C 'compact':503C 'comput':492C 'configur':541C 'convinc':300C 'cooki':185C,201C,268C,466C,476C,505C,516C 'could':208C,225C 'detail':448C 'digit':583C 'dot':584C 'email':193C,244C,257C,512C 'email-chang':511C 'enabl':198C,270C 'endpoint':154C,167C,207C,348C,450C,537C 'entri':134C,222C 'exact':418C 'example.com':140C 'expo':109C,149C,363C,389C,396C 'extern':214C 'fail':372C 'fix':355C,522C 'forg':510C 'form':440C 'generic':543C 'help':156C 'hmac':481C 'hmac-sha256':480C 'host':232C,560C,579C 'hs256':495C 'https':215C 'id':278C 'ident':370C,377C 'impact':235C 'intend':230C 'ipv4':593C 'issu':147C,352C,436C,473C,530C 'json':496C 'key':483C 'know':240C,274C 'later':119C 'let':305C 'line':339C 'link':195C,291C 'longer':565C 'look':298C 'made':580C 'make':289C 'match':226C,553C,566C,591C 'mean':311C 'need':124C,131C 'oauth':544C 'one':572C 'open':203C,286C 'origin':127C,219C,297C,317C,380C,538C,551C 'otherwis':399C 'outsid':228C 'pars':558C,576C 'pass':314C 'patch':319C 'path':234C,408C,562C 'phish':290C 'plugin':110C,150C,398C 'problem':128C,303C,445C 'provid':371C,378C,548C 'public':153C 'reachabl':350C 'read':451C 'redirect':204C,209C,287C,532C 'regist':107C 'remov':394C 'request':174C,401C 'return':180C 'rotat':342C 'scheme':231C,559C 'secret':178C,191C,345C,360C,487C 'server':106C,397C,471C,529C 'session':200C,267C,504C 'set':122C 'sha256':482C 'sign':142C,164C,168C,247C,365C,392C,444C,465C,523C 'sign-in':163C,364C,391C 'signatur':182C,351C,477C,499C,507C 'site':216C 'social':162C,390C,542C 'sso':369C,547C 'start':160C,293C 'state':413C,431C,452C,527C 'stay':356C 'store':461C 'summari':141C 'suppli':457C 'system':158C 'taken':171C 'technic':447C 'token':498C,514C 'trust':218C,313C,316C,546C,550C 'trustedorigin':136C,224C,382C 'unauthent':237C 'unexpir':526C 'upgrad':320C,341C,374C 'url':227C,307C,459C,555C,568C 'use':188C,388C 'user':242C,251C,264C,276C,284C 'userinfo':570C 'valid':357C 'valu':143C,170C,414C,432C,518C 'verif':194C 'visitor':211C 'web':497C 'whose':409C 'wildcard':126C,133C,217C,221C,302C,549C,578C,586C 'without':467C 'workaround':383C 'z':424C,427C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-11-03T21:37:25+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-4wq4-2ppv-q3hg","url":"https://github.com/better-auth/better-auth/security/advisories/GHSA-4wq4-2ppv-q3hg","severity":"critical","published_at":"2026-11-03T21:37:25+00:00","modified_at":"2026-11-03T21:37:25+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":63804,"url":"https://github.com/better-auth/better-auth/security/advisories/GHSA-4wq4-2ppv-q3hg","title":"Vendor advisory: github.com","kind":"vendor","source":"nvd","publisher":"vendor","text":null,"published_at":"2026-11-03T21:37:25+00:00","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"}