Stripe's decline_code field on a StripeCardError carries values like stolen_card, fraudulent, card_velocity_exceeded, and insufficient_funds. If your API forwards this code to the browser, you are giving card-testing bots a real-time oracle: they can distinguish cards that are blocked as stolen from cards that are simply declined for insufficient funds, and continue probing with the latter. CWE-209 (Information Exposure Through Error Message) and CWE-200 (Exposure of Sensitive Information) both apply. OWASP A05 (Security Misconfiguration) covers this as information leakage. The internal decline_code is a legitimate signal for your fraud team — it should flow to your logging pipeline, not to the client.
Medium because exposing decline codes to clients gives card-testing bots a free oracle to distinguish stolen-and-blocked cards from valid-but-declined ones, improving attack efficiency.
Log the decline_code server-side for your fraud and support teams, and return a single generic message regardless of the specific decline reason.
} catch (err) {
if (err instanceof Stripe.errors.StripeCardError) {
// Internal: log the specific code for fraud pattern analysis
console.error('Card declined', {
decline_code: err.decline_code, // 'stolen_card', 'insufficient_funds', etc.
code: err.code,
paymentIntentId,
})
// External: generic message regardless of decline reason
return NextResponse.json(
{ error: 'Your payment was declined. Please contact your card issuer or try a different card.' },
{ status: 402 }
)
}
}
Do not map specific decline codes to user-facing messages like "Insufficient funds" — that narrows the attack surface for carding bots by confirming the card number is valid.
ID: ecommerce-payment-security.fraud-prevention.decline-codes-internal-only
Severity: medium
What to look for: Check error handling code for payment API calls specifically around decline scenarios. For Stripe, a declined card results in a StripeCardError with a decline_code property (values like insufficient_funds, card_velocity_exceeded, stolen_card, fraudulent). Search for where decline_code is accessed and whether that value or an equivalent gets serialized into the API response body. Count all error response paths that handle card declines and for each, verify decline_code stays server-side only.
Pass criteria: Decline codes are captured and logged server-side (e.g., written to application logs or a monitoring service). The client receives only a generic decline message without the specific code. Different decline types produce the same generic user-facing message. No more than 0 decline code values should appear in API response bodies.
Fail criteria: The specific decline code (e.g., stolen_card, fraudulent, insufficient_funds) is included in the API response body and displayed to the user. This can help fraudsters understand which card numbers are flagged as stolen vs. just declined.
Skip (N/A) when: Never — decline code handling applies to all projects that process card payments.
Detail on fail: Describe what is exposed. Example: "Error response JSON includes { declineCode: 'stolen_card' } — exposing this to the client helps card testers distinguish blocked cards from valid-but-declined ones"
Remediation: Log decline codes server-side and return only generic messages:
} catch (err) {
if (err instanceof Stripe.errors.StripeCardError) {
// Log internally — never expose decline_code to client
console.error('Card declined', {
decline_code: err.decline_code, // 'insufficient_funds', 'stolen_card', etc.
code: err.code,
paymentIntentId,
})
// Generic message to client regardless of decline reason
return NextResponse.json(
{ error: 'Your payment was declined. Please contact your card issuer or try a different card.' },
{ status: 402 }
)
}
}
Avoid mapping specific decline codes to user-facing messages like "insufficient funds" — this narrows the attack surface for card-testing bots.