CVV Stored in Cleartext via GraphQL API - PCI DSS Violation
The Risk
The app stored customers' full credit card numbers and CVV security codes in its own database and returned them in plaintext through its API. This is the equivalent of a shop writing down every customer's card details in a notebook and leaving it on the counter. Any breach of the backend would expose complete, ready-to-use card data for every user who saved a card. This violates the most fundamental rule in payment card security: never store the CVV.
The Vulnerability
Static analysis of the Android app (JADX decompilation and React Native bundle analysis) revealed a complete absence of any payment tokenization. No payment SDK was present: no Stripe, no Braintree, no Adyen, no processor SDK of any kind. Raw card data flowed directly from the app to the company's own backend.
Three confirmed issues
1. CVV persisted and returned in cleartext
The saveCard GraphQL mutation accepted cardCvv as a string parameter and stored it in the database. The CurrentCards query returned the CVV alongside the full unmasked card number. Only a session bearer token was required. No PIN, no 2FA, no re-authentication.
2. Card data round-tripped through injectable JavaScript
A getInjectScript query accepted raw PAN, CVV, and expiry as GraphQL variables. The server processed these and returned a JavaScript snippet with the card credentials embedded as string literals, designed to auto-fill forms on four third-party payment gateways.
3. No tokenization architecture exists
The HTTP client used a bare constructor with no certificate pinning and cleartext traffic enabled in the manifest. The entire payment flow bypassed industry-standard tokenization. The backend stored, processed, and transmitted raw cardholder data instead of delegating to a PCI-compliant processor.
The Attack
The proof of concept demonstrated the full round-trip using the researcher's own authenticated session:
- Save a card via the
saveCardmutation, providing a full card number, expiry, and CVV - Query
CurrentCards. The API returns all saved cards with full PAN and CVV in cleartext - Call
getInjectScriptwith the same card data. The server returns a JavaScript block containing the credentials as embedded strings - Inspect the returned JavaScript. It contains auto-fill logic for four different payment gateway domains with the raw card data
The vulnerability doesn't require exploiting another user's account. The issue is that CVV data exists in storage at all. PCI DSS Requirement 3.2 explicitly prohibits storing CVV/CVC2 data after authorization, under any circumstances, even if encrypted.
The Impact
PCI DSS violations
Requirement 3.2: CVV must never be stored post-authorization. Requirement 3.4: PAN must be rendered unreadable in storage. Requirement 4.1: Cardholder data must be encrypted in transit (cleartext traffic was enabled). Any single one of these would be a compliance failure.
Mass card exposure risk
Any backend compromise, SQL injection, or insider threat would expose complete, ready-to-use card data (PAN + CVV + expiry) for every user who saved a card. No cracking or decryption required.
Card fraud enablement via JavaScript injection
The getInjectScript flow created a server-side template injection surface. If the auto-fill JavaScript was intercepted or the gateway domains were spoofed, card data could be silently redirected.
Remediation
- Immediately stop storing CVV data. Delete all CVV values from the database
- Integrate a PCI-compliant payment processor (Stripe, Braintree, Adyen) and use their tokenization SDKs
- Replace the
saveCardflow with processor-issued card tokens. Never send raw card data to your own backend - Remove the
getInjectScriptendpoint entirely. Card data should never be embedded in server-generated JavaScript - Enable certificate pinning and disable cleartext traffic in the Android manifest
- Engage a Qualified Security Assessor (QSA) for PCI DSS compliance review