Returning raw exception messages, Stripe error objects, or database errors from payment API routes leaks internal system architecture to every browser that inspects network responses. A response like { error: "No such payment_intent: 'pi_3Ox...'" } tells an attacker your payment intent ID format, your Stripe account configuration, and the exact server-side path that failed. CWE-209 (Generation of Error Message Containing Sensitive Information) directly names this pattern. OWASP A05 (Security Misconfiguration) flags information leakage from unhandled exceptions as a reconnaissance vector. Beyond security, raw Stripe error messages contain decline codes and internal identifiers that help card-testing bots distinguish working cards from flagged ones.
Medium because raw exception messages in API responses expose internal system details and Stripe error internals that serve as reconnaissance data for card-testing attacks and targeted exploits.
Catch and classify errors in payment routes; return only generic user-facing messages while logging the full details server-side.
// app/api/payment/route.ts
try {
const paymentIntent = await stripe.paymentIntents.confirm(id)
return NextResponse.json({ status: paymentIntent.status })
} catch (err) {
if (err instanceof Stripe.errors.StripeCardError) {
console.error('Card error', { code: err.code, decline_code: err.decline_code, id })
return NextResponse.json(
{ error: 'Your card was declined. Please try a different payment method.' },
{ status: 402 }
)
}
console.error('Payment error', err)
return NextResponse.json(
{ error: 'Payment could not be processed. Please try again.' },
{ status: 500 }
)
}
Never return err.message, err.stack, or a serialized error object in the response body. The safe surface to the client is a human-readable string you control.
ID: ecommerce-payment-security.fraud-prevention.generic-error-messages
Severity: medium
What to look for: Count every error response path in payment API routes. For each catch block or error handler, classify the response as safe (generic message like "Payment failed") or unsafe (raw err.message, err.stack, or full error object serialization). Check for patterns like return NextResponse.json({ error: err.message }) or res.status(500).json(err) that would forward raw error messages. Report the ratio: "X of Y error paths return safe generic messages."
Pass criteria: 100% of payment-related API error responses return generic, user-friendly messages that do not reveal internal system details, stack traces, database error messages, or payment provider error internals. No more than 0 error paths should expose raw exception details. Detailed error information is logged server-side only.
Fail criteria: Any payment API error response returns a raw exception message, a database error string, a stack trace, or an unfiltered Stripe error object to the client.
Do NOT pass when: Error responses include err.message from Stripe errors — Stripe error messages contain internal details like "No such payment_intent: 'pi_xxx'" that should not reach the client. Forwarding err.message is NOT a pass.
Skip (N/A) when: Never — this applies to all payment-related API routes.
Detail on fail: Describe the exposed information. Example: "POST /api/payment returns the full Stripe StripeCardError object including decline_code and charge ID in the JSON response — internal payment details are exposed to the browser"
Cross-reference: The Error & Resilience audit covers error exposure and information leakage across all API routes, not limited to payment endpoints.
Remediation: Sanitize errors before sending to the client. Log detailed errors server-side:
// app/api/payment/route.ts
export async function POST(req: Request) {
try {
const paymentIntent = await stripe.paymentIntents.confirm(paymentIntentId)
return NextResponse.json({ status: paymentIntent.status })
} catch (err) {
if (err instanceof Stripe.errors.StripeCardError) {
// Log the full error server-side for diagnostics
console.error('Card error', { code: err.code, decline_code: err.decline_code })
// Return only a safe, generic message to the client
return NextResponse.json(
{ error: 'Your card was declined. Please try a different payment method.' },
{ status: 402 }
)
}
// Generic server error — do not expose internals
console.error('Payment processing error', err)
return NextResponse.json(
{ error: 'Payment could not be processed. Please try again.' },
{ status: 500 }
)
}
}