- ID
- ef3e755c-46a6-4a0f-8fc3-a09e6a213f09
BE-7373 SMS opt-in/opt-out endpoints allow subscribing arbitrary phone numbers via Braze
- source :: https://hungryroot.atlassian.net/browse/BE-7373
- tags :: Hungryroot API Project
- epic :: EP-608 Compliance on Checkout
- sprint :: HR BE Sprint 105
- status :: Status - Complete
Tasks
DONE Come up with a competent way to test that checking numbers works
- ID
- 8139ba8b-7cca-5f9f-63a6-122482efe4c6
DONE Open a cherry pick to get this change on staging tomorrow
- ID
- adee4abc-1c9d-06f5-1661-8a3b1a655af2
Description
][Google Doc]]
Problem
The POST /api/v2/customers/self/sms_opt_in/ endpoint and the PATCH
/api/v2/customers/{id}/address/ endpoint (with sms_opt_in: true) both issue calls
to Braze to opt-in or opt-out a phone number without verifying that the
authenticated customer actually owns the phone number in our system.
This means any authenticated user (including free quiz signups with just email +
password, no payment) can subscribe or unsubscribe arbitrary phone numbers.
Attack Vectors
Vector 1: Mass Opt-In (Both Dynamic Config ON & OFF States)
An attacker can enumerate through phone numbers and subscribe them to SMS
without the phone number owner's knowledge.
Steps:
1. Progress through the quiz and create an account (email + password only, no
payment)
2. With dynamic config ENABLE_BRAZE_SMS_OPTIN_ENDPOINT ON: Call POST
/api/v2/customers/self/sms_opt_in/ with any valid 10-digit US phone and
sms_opt_in: true
3. With config OFF: Call PATCH /api/v2/customers/{id}/address/ with valid address
data including sms_opt_in: true
4. The phone is immediately subscribed in Braze
Protections in place: None meaningful. Authentication is required but a free
quiz signup qualifies.
Proposed Fix
Before issuing calls to Braze for sms_opt_out or sms_opt_in, validate that the
phone number provided in the request matches a phone number already associated
with the customer in our database (e.g. on their Address record).
Affected Code
app/rest/customer.py - sms_opt_in action (lines ~1293-1327): calls
braze.sms_opt_in() / braze.sms_opt_out() without checking phone ownership
app/rest/address.py - address update path with sms_opt_in field
Larger Issue (Note)
There is a broader problem: we don't currently validate that the person signing
up owns the phone number they are providing us. There is no phone verification
(e.g. OTP/SMS confirmation) during account creation or address entry. This
ticket addresses the immediate fix of checking existing phone ownership before
issuing Braze calls; the broader phone verification gap should be tracked
separately.
Acceptance Criteria
- [ ] sms_opt_in endpoint validates the phone number belongs to the customer
before calling Braze
- [ ] Address update path with sms_opt_in validates phone ownership before calling
Braze
- [ ] If the phone number doesn't match the customer's stored phone, return a 400
error
- [ ] Unit tests cover both the happy path and the rejection path