ct manipulation possible) | High (Manual cURL replay & log scanning required) |
| Server-Determined Pricing | Near Zero (Allowlist + env var mapping) | 0.0% (Static plans unforgeable) | Low (Automated boundary checks suffice) |
Key Findings:
- Server-side allowlists reduce the static pricing attack surface by 100%.
- Dynamic pricing remains viable but requires explicit server-side bounds validation before reaching Stripe's API.
- The sweet spot: Use
price references with environment variables for fixed tiers, and validated numeric ranges for custom/dynamic amounts. Never mix client-supplied monetary values with inline price_data for standard plans.
Core Solution
Implementation Principle: The client declares intent (which plan). The server enforces reality (what that plan costs).
Static Plan Mapping (Server-Determined Pricing):
// app/api/checkout/route.ts (server-determined pricing)
const PLANS = {
hobby: { priceId: process.env.STRIPE_PRICE_HOBBY },
premium: { priceId: process.env.STRIPE_PRICE_PREMIUM },
enterprise: { priceId: process.env.STRIPE_PRICE_ENTERPRISE },
} as const;
type PlanKey = keyof typeof PLANS;
export async function POST(req: Request) {
const { plan } = (await req.json()) as { plan: PlanKey };
// 1. Validate the plan key against a server-side allowlist
if (!Object.hasOwn(PLANS, plan)) {
return new Response("Invalid plan", { status: 400 });
}
// 2. Look up the priceId server-side. Never accept it from the client.
const { priceId } = PLANS[plan];
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${origin}/success`,
cancel_url: `${origin}/cancel`,
});
return Response.json({ url: session.url });
}
Dynamic Amount Handling (Validated Bounds):
// Dynamic amount, still server-determined
const { donationCents } = await req.json();
if (
typeof donationCents !== "number" ||
donationCents < 100 ||
donationCents > 100000
) {
return new Response("Invalid amount", { status: 400 });
}
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [
{
price_data: {
currency: "eur",
product_data: { name: "Donation" },
unit_amount: donationCents,
},
quantity: 1,
},
],
// ...
});
Verification Protocol:
# 1. Open your /pricing page. Click your most expensive plan.
# Watch the Network tab when you hit "Subscribe" or "Buy".
# 2. Find the request to your checkout-create endpoint. Copy it as cURL.
# 3. Replay it with a tampered body. Change priceId, amount, plan name,
# quantity, anything money-shaped:
curl -X POST https://yoursite.com/api/checkout \
-H "Content-Type: application/json" \
-H "Cookie: <your auth cookie>" \
-d '{"plan":"premium","priceId":"price_FAKE","amount":1,"quantity":-1}'
# 4. Check the response. If you got a Stripe Checkout URL, open it.
# If the price shown is anything other than your real plan price, you have a bug.
Pitfall Guide
- Direct
amount Injection: Passing client-controlled amount directly into unit_amount bypasses all business logic. Stripe will charge exactly what it receives, enabling β¬1 purchases for premium tiers.
- Unvalidated
priceId Swapping: Trusting client-supplied priceId allows attackers to downgrade expensive plans or swap to internal/legacy price objects. Always resolve plan identifiers to Stripe IDs server-side.
- Quantity Boundary Violations: Negative or zero quantities can trigger unexpected billing behavior or negative charge amounts in older API versions. Explicitly validate
quantity >= 1 before session creation.
- Blind Coupon/Promo Code Application: Passing client-supplied promo codes directly to Stripe without server-side verification allows unauthorized discounts. Validate code existence, active status, plan eligibility, and user constraints server-side.
- Client-Supplied
customerId: Allowing clients to attach checkout sessions to arbitrary Stripe customer IDs enables account takeover or billing attribution hijacking. Always derive customerId from authenticated server state.
- Misusing
price_data for Static Plans: Using inline price_data for fixed-tier plans duplicates pricing logic and increases tampering risk. Reserve price_data for truly dynamic or marketplace-specific scenarios; use pre-configured Stripe price objects for everything else.
Deliverables
π¦ Stripe Checkout Security Blueprint
A complete architectural reference covering environment variable mapping strategies, server-side allowlist implementation patterns, dynamic pricing boundary validation, and webhook reconciliation flows. Includes threat modeling matrices for payment endpoints.
β
Pre-Deployment Pricing Audit Checklist
A step-by-step verification protocol for engineering teams: