{"id":"GHSA-x8j7-xh58-rv6x","kind":"vulnerability","aliases":["GHSA-x8j7-xh58-rv6x"],"related":[],"title":"@better-auth/stripe: Billing actions can target another customer's Stripe account","description":"## Am I affected?\n\nYou are affected if you register the `stripe()` plugin from `@better-auth/stripe` 1.2.0-beta.18 or later. The billing effects need `subscription.enabled: true`.\n\n## Summary\n\nThe Stripe plugin stores each user's Stripe customer ID on the user record. That field accepted values from sign-up and profile update requests, so a user could replace their own customer ID with another customer's. The billing portal, checkout, and webhook handling then treated that customer as theirs.\n\n## Impact\n\nA signed-in user who knows another customer's Stripe ID can open the billing portal for that customer. There they can see invoices and billing details, change payment methods, and cancel or change subscriptions. A subscription created for that customer outside checkout could also be attributed to the attacker. The flaw does not sign anyone in as another user or expose Stripe API keys.\n\n## Patches\n\nUpgrade `@better-auth/stripe` to 1.7.16, or to 1.8.0-beta.5 on the beta line.\n\nCustomer IDs and the subscription fields the plugin manages can no longer be set from sign-up or update-user requests, including server calls such as `auth.api.updateUser`. Write them through your database adapter instead.\n\nThe upgrade does not change values written before it. Check for customer IDs held by more than one user (see Workarounds).\n\n## Workarounds\n\nAdd `databaseHooks.user.create.before` and `databaseHooks.user.update.before` hooks that remove `stripeCustomerId` from incoming data.\n\nTo find values written before the fix, look for customer IDs held by more than one user. For example, on PostgreSQL with the default schema names:\n\n```sql\nSELECT \"stripeCustomerId\", array_agg(\"id\") AS user_ids\nFROM \"user\"\nWHERE \"stripeCustomerId\" IS NOT NULL\nGROUP BY \"stripeCustomerId\"\nHAVING count(*) > 1;\n```\n\nCompare each row with the Stripe customer's `metadata.userId`. Run the same check on `organization` if organization billing is enabled.\n\n## Technical details\n\n`stripeCustomerId` had no `input: false`, so the generic user routes accepted it. The billing routes now also take the customer type from the authorized reference instead of the request body. When several records share a customer ID, the `customer.subscription.created` webhook now attributes the subscription to the owner recorded on the Stripe customer, and skips it with a warning when no recorded owner holds the ID.","summary":"The @better-auth/stripe plugin accepted a Stripe customer ID from sign-up and profile update requests, so a signed-in user could point their account at someone else's Stripe customer. That lets them open that customer's billing portal, view invoices, change payment methods and cancel or change subscriptions.","severity":"medium","cvss":5.9,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"@better-auth/stripe","fixed":["1.7.16"],"affected":">= 1.2.0-beta.18, < 1.7.16","ecosystem":"npm"},{"name":"@better-auth/stripe","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-639","cwe-915","node","package","saas-billing","stripe"],"not_affected_if":["managed-hosting","no-stripe-plugin","subscription-disabled"],"how_to_check":"- Check the installed version with `npm ls @better-auth/stripe`; affected ranges are `>= 1.2.0-beta.18, < 1.7.16` and `>= 1.8.0-beta.0, < 1.8.0-beta.5`.\n- Confirm the `stripe()` plugin is registered in your auth configuration; the advisory says billing effects need `subscription.enabled: true`.\n- Look for Stripe customer IDs held by more than one user with the PostgreSQL query in the advisory (`SELECT \"stripeCustomerId\", array_agg(\"id\") ... HAVING count(*) > 1`).","fix":"- `@better-auth/stripe` `1.7.16`\n- `@better-auth/stripe` `1.8.0-beta.5` on the beta line\n- Until upgraded, the advisory suggests `databaseHooks.user.create.before` and `databaseHooks.user.update.before` hooks that strip `stripeCustomerId` from incoming data; the upgrade does not change values written before it.","status":"open","published_at":"2026-11-03T21:39:30+00:00","first_seen_at":"2026-11-09T22:53:20+00:00","triaged_at":"2026-11-09T22:56:13+00:00","updated_at":"2026-11-09T22:56:13+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-11-09T22:56:13Z","model":"claude-haiku-5-5","output":{"fix":"- `@better-auth/stripe` `1.7.16`\n- `@better-auth/stripe` `1.8.0-beta.5` on the beta line\n- Until upgraded, the advisory suggests `databaseHooks.user.create.before` and `databaseHooks.user.update.before` hooks that strip `stripeCustomerId` from incoming data; the upgrade does not change values written before it.","reach":"internet","title":"better-auth Stripe plugin lets users bill another customer's Stripe account","impact":"medium","summary":"The @better-auth/stripe plugin accepted a Stripe customer ID from sign-up and profile update requests, so a signed-in user could point their account at someone else's Stripe customer. That lets them open that customer's billing portal, view invoices, change payment methods and cancel or change subscriptions.","audience":"developers","category":"package","nicknames":[],"who_cares":["better-auth","stripe","node","saas-billing"],"confidence":0.8,"fix_status":"available","needs_auth":"yes","worst_case":"A signed-in user manages another customer's billing and subscriptions","analyst_note":"","how_to_check":"- Check the installed version with `npm ls @better-auth/stripe`; affected ranges are `>= 1.2.0-beta.18, < 1.7.16` and `>= 1.8.0-beta.0, < 1.8.0-beta.5`.\n- Confirm the `stripe()` plugin is registered in your auth configuration; the advisory says billing effects need `subscription.enabled: true`.\n- Look for Stripe customer IDs held by more than one user with the PostgreSQL query in the advisory (`SELECT \"stripeCustomerId\", array_agg(\"id\") ... HAVING count(*) > 1`).","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["no-stripe-plugin","subscription-disabled","managed-hosting"],"who_is_affected":"Applications that register the stripe() plugin from @better-auth/stripe with subscriptions enabled are exposed, because sign-up and profile update requests could set the Stripe customer ID. Apps that do not use the plugin, or that leave billing disabled, are not affected. Values written before the upgrade remain and should be checked for IDs shared by more than one user.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"unknown"},"cost_usd":0.00096,"input_tokens":5414,"output_tokens":838,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-639':36B '-915':38B '/stripe':4A,27A,31A,49B,115C,251C '1':378C '1.2.0':116C '1.7.16':253C '1.8.0':256C 'accept':51B,143C,411C 'account':13A,73B 'action':6A 'adapt':296C 'add':320C 'affect':101C,104C 'agg':361C 'also':225C,417C 'anoth':9A,163C,187C,239C 'anyon':236C 'api':244C 'array':360C 'attack':230C 'attribut':227C,442C 'auth':3A,26A,30A,34B,48B,114C,250C 'auth.api.updateuser':290C 'author':424C 'beta':260C 'beta.18':117C 'beta.5':257C 'better':2A,25A,29A,33B,47B,113C,249C 'better-auth':1A,24A,28A,32B,46B,112C,248C 'bill':5A,43B,87B,121C,167C,195C,206C,396C,414C 'bodi':430C 'call':287C 'cancel':95B,212C 'chang':91B,97B,208C,214C,302C 'check':307C,391C 'checkout':169C,223C 'compar':379C 'could':70B,156C,224C 'count':377C 'creat':218C 'custom':10A,54B,79B,85B,135C,160C,164C,176C,188C,199C,221C,262C,309C,340C,385C,420C,436C,452C 'customer.subscription.created':439C 'cwe':35B,37B 'data':330C 'databas':295C 'databasehooks.user.create.before':321C 'databasehooks.user.update.before':323C 'default':354C 'detail':207C,400C 'effect':122C 'els':76B 'enabl':398C 'exampl':349C 'expos':242C 'fals':405C 'field':142C,267C 'find':332C 'fix':337C 'flaw':232C 'generic':408C 'ghsa':15A,20A 'ghsa-x8j7-xh58-rv6x':14A,19A 'group':373C 'handl':172C 'held':311C,342C 'hold':463C 'hook':324C 'id':55B,136C,161C,191C,263C,310C,341C,362C,365C,437C,465C 'impact':179C 'includ':285C 'incom':329C 'input':404C 'instead':297C,426C 'invoic':90B,204C 'key':245C 'know':186C 'later':119C 'let':81B 'line':261C 'longer':273C 'look':338C 'manag':270C 'metadata.userid':387C 'method':93B,210C 'name':356C 'need':123C 'node':39B 'null':372C 'one':315C,346C 'open':83B,193C 'organ':393C,395C 'outsid':222C 'owner':447C,462C 'packag':40B 'patch':246C 'payment':92B,209C 'plugin':50B,110C,129C,269C 'point':71B 'portal':88B,168C,196C 'postgresql':351C 'profil':61B,150C 'record':140C,433C,448C,461C 'refer':425C 'regist':107C 'remov':326C 'replac':157C 'request':63B,152C,284C,429C 'rout':410C,415C 'row':381C 'run':388C 'rv6x':18A,23A 'saa':42B 'saas-bil':41B 'schema':355C 'see':203C,317C 'select':358C 'server':286C 'set':275C 'sever':432C 'share':434C 'sign':58B,67B,147C,182C,235C,278C 'sign-up':57B,146C,277C 'signed-in':66B,181C 'skip':454C 'someon':75B 'sql':357C 'store':130C 'stripe':12A,44B,53B,78B,109C,128C,134C,190C,243C,384C,451C 'stripecustomerid':327C,359C,369C,375C,401C 'subscript':98B,215C,217C,266C,444C 'subscription.enabled':124C 'summari':126C 'take':418C 'target':8A 'technic':399C 'treat':174C 'true':125C 'type':421C 'updat':62B,151C,282C 'update-us':281C 'upgrad':247C,299C 'user':69B,132C,139C,155C,184C,240C,283C,316C,347C,364C,367C,409C 'valu':144C,303C,333C 'view':89B 'warn':458C 'webhook':171C,440C 'workaround':318C,319C 'write':291C 'written':304C,334C 'x8j7':16A,21A 'xh58':17A,22A","nicknames":[],"mitigations":[],"who_affected":"Applications that register the stripe() plugin from @better-auth/stripe with subscriptions enabled are exposed, because sign-up and profile update requests could set the Stripe customer ID. Apps that do not use the plugin, or that leave billing disabled, are not affected. Values written before the upgrade remain and should be checked for IDs shared by more than one user.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["stripe"],"ptokens":["auth","better","stripe"],"labels":["betterauth","cwe639","cwe915","medium","node","npm","package","saasbilling","stripe"],"campaign_type":null,"search_a":"'/stripe':4A,27A,31A 'account':13A 'action':6A 'anoth':9A 'auth':3A,26A,30A 'better':2A,25A,29A 'better-auth':1A,24A,28A 'bill':5A 'custom':10A 'ghsa':15A,20A 'ghsa-x8j7-xh58-rv6x':14A,19A 'rv6x':18A,23A 'stripe':12A 'target':8A 'x8j7':16A,21A 'xh58':17A,22A","search_b":"'-639':36B '-915':38B '/stripe':49B 'accept':51B 'account':73B 'auth':34B,48B 'better':33B,47B 'better-auth':32B,46B 'bill':43B,87B 'cancel':95B 'chang':91B,97B 'could':70B 'custom':54B,79B,85B 'cwe':35B,37B 'els':76B 'id':55B 'invoic':90B 'let':81B 'method':93B 'node':39B 'open':83B 'packag':40B 'payment':92B 'plugin':50B 'point':71B 'portal':88B 'profil':61B 'request':63B 'saa':42B 'saas-bil':41B 'sign':58B,67B 'sign-up':57B 'signed-in':66B 'someon':75B 'stripe':44B,53B,78B 'subscript':98B 'updat':62B 'user':69B 'view':89B","search_c":"'/stripe':115C,251C '1':378C '1.2.0':116C '1.7.16':253C '1.8.0':256C 'accept':143C,411C 'adapt':296C 'add':320C 'affect':101C,104C 'agg':361C 'also':225C,417C 'anoth':163C,187C,239C 'anyon':236C 'api':244C 'array':360C 'attack':230C 'attribut':227C,442C 'auth':114C,250C 'auth.api.updateuser':290C 'author':424C 'beta':260C 'beta.18':117C 'beta.5':257C 'better':113C,249C 'better-auth':112C,248C 'bill':121C,167C,195C,206C,396C,414C 'bodi':430C 'call':287C 'cancel':212C 'chang':208C,214C,302C 'check':307C,391C 'checkout':169C,223C 'compar':379C 'could':156C,224C 'count':377C 'creat':218C 'custom':135C,160C,164C,176C,188C,199C,221C,262C,309C,340C,385C,420C,436C,452C 'customer.subscription.created':439C 'data':330C 'databas':295C 'databasehooks.user.create.before':321C 'databasehooks.user.update.before':323C 'default':354C 'detail':207C,400C 'effect':122C 'enabl':398C 'exampl':349C 'expos':242C 'fals':405C 'field':142C,267C 'find':332C 'fix':337C 'flaw':232C 'generic':408C 'group':373C 'handl':172C 'held':311C,342C 'hold':463C 'hook':324C 'id':136C,161C,191C,263C,310C,341C,362C,365C,437C,465C 'impact':179C 'includ':285C 'incom':329C 'input':404C 'instead':297C,426C 'invoic':204C 'key':245C 'know':186C 'later':119C 'line':261C 'longer':273C 'look':338C 'manag':270C 'metadata.userid':387C 'method':210C 'name':356C 'need':123C 'null':372C 'one':315C,346C 'open':193C 'organ':393C,395C 'outsid':222C 'owner':447C,462C 'patch':246C 'payment':209C 'plugin':110C,129C,269C 'portal':168C,196C 'postgresql':351C 'profil':150C 'record':140C,433C,448C,461C 'refer':425C 'regist':107C 'remov':326C 'replac':157C 'request':152C,284C,429C 'rout':410C,415C 'row':381C 'run':388C 'schema':355C 'see':203C,317C 'select':358C 'server':286C 'set':275C 'sever':432C 'share':434C 'sign':147C,182C,235C,278C 'sign-up':146C,277C 'signed-in':181C 'skip':454C 'sql':357C 'store':130C 'stripe':109C,128C,134C,190C,243C,384C,451C 'stripecustomerid':327C,359C,369C,375C,401C 'subscript':215C,217C,266C,444C 'subscription.enabled':124C 'summari':126C 'take':418C 'technic':399C 'treat':174C 'true':125C 'type':421C 'updat':151C,282C 'update-us':281C 'upgrad':247C,299C 'user':132C,139C,155C,184C,240C,283C,316C,347C,364C,367C,409C 'valu':144C,303C,333C 'warn':458C 'webhook':171C,440C 'workaround':318C,319C 'write':291C 'written':304C,334C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-11-03T21:39:30+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-x8j7-xh58-rv6x","url":"https://github.com/better-auth/better-auth/security/advisories/GHSA-x8j7-xh58-rv6x","severity":"medium","published_at":"2026-11-03T21:39:30+00:00","modified_at":"2026-11-03T21:39:30+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":110502,"url":"https://github.com/better-auth/better-auth/security/advisories/GHSA-x8j7-xh58-rv6x","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"}