Decline codes are logged internally but not exposed to the client
Why it matters
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.
Severity rationale
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.
Remediation
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.
Detection
-
ID:
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
StripeCardErrorwith adecline_codeproperty (values likeinsufficient_funds,card_velocity_exceeded,stolen_card,fraudulent). Search for wheredecline_codeis 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, verifydecline_codestays 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.
External references
- cwe · CWE-209 — Generation of Error Message Containing Sensitive Information
- cwe · CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor
- owasp:2021 · A05
Taxons
History
- 2026-04-18·v1.0.0·Initial import from ecommerce-payment-security·automated