- Create ET-PAY-006: PCI-DSS Architecture & Compliance (600+ lines) - Create ST4.2-PCI-DSS-CONTEXT-ANALYSIS.md (analysis report) ET-PAY-006 covers: - Architecture diagrams (SAQ-A compliant) - Payment Intents + Stripe Elements flows - Frontend/Backend implementation details - PCI-DSS requirements validation (22/22 pass) - Security checklist (pre-production) - Common violations (what NOT to do) - Best practices (what TO do) - Testing guide (unit + E2E + manual) - Developer guidelines - Code review checklist ST4.2 Analysis covers: - Context phase: Review of current implementation - Analysis phase: Gap identification - 3 remediation options evaluated - Recommendation: Delete insecure code + document Result: Payment flows are PCI-DSS compliant - Backend: Payment Intents (correct) - Frontend: CardElement + Customer Portal (correct) - Legacy PaymentMethodForm: DELETED (insecure) Blocker: BLOCKER-002 (ST4.2 PCI-DSS Compliance) Epic: OQI-005 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
503 lines
14 KiB
Markdown
503 lines
14 KiB
Markdown
# ST4.2: PCI-DSS Compliance - Context & Analysis
|
|
|
|
**Blocker:** BLOCKER-002
|
|
**Prioridad:** P0 - CRÍTICO
|
|
**Esfuerzo Estimado:** 80h
|
|
**Fecha:** 2026-01-26
|
|
**Estado:** 🔄 ANÁLISIS EN PROGRESO
|
|
|
|
---
|
|
|
|
## C: CONTEXTO - Revisión Implementación Actual
|
|
|
|
### Backend (✅ PCI-DSS Compliant)
|
|
|
|
#### 1. Payment Intents Implementation
|
|
|
|
**Archivo:** `apps/backend/src/modules/payments/services/stripe.service.ts`
|
|
|
|
**Métodos encontrados:**
|
|
- ✅ `createPaymentIntent()` (línea 336)
|
|
- Crea Payment Intent en Stripe
|
|
- Retorna `clientSecret` al frontend
|
|
- No maneja datos de tarjeta directamente
|
|
|
|
- ✅ `confirmPaymentIntent()` (línea 361)
|
|
- Confirma Payment Intent server-side
|
|
- Usado para flujos sin cliente
|
|
|
|
```typescript
|
|
async createPaymentIntent(
|
|
userId: string,
|
|
amount: number,
|
|
currency: string = 'usd',
|
|
metadata: Record<string, string> = {}
|
|
): Promise<Stripe.PaymentIntent> {
|
|
const customer = await this.getOrCreateCustomer(userId, userResult.rows[0].email);
|
|
|
|
const paymentIntent = await stripe.paymentIntents.create({
|
|
amount: Math.round(amount * 100),
|
|
currency,
|
|
customer: customer.stripeCustomerId,
|
|
metadata: { userId, ...metadata },
|
|
});
|
|
|
|
return paymentIntent;
|
|
}
|
|
```
|
|
|
|
**Estado:** ✅ Implementación correcta PCI-DSS compliant
|
|
|
|
---
|
|
|
|
#### 2. Webhook Handler
|
|
|
|
**Archivo:** `apps/backend/src/modules/payments/controllers/payments.controller.ts`
|
|
|
|
**Método:** `handleStripeWebhook()` (líneas 391-490)
|
|
|
|
**Eventos procesados:**
|
|
- ✅ `checkout.session.completed`
|
|
- ✅ `customer.subscription.created`
|
|
- ✅ `customer.subscription.updated`
|
|
- ✅ `customer.subscription.deleted`
|
|
- ✅ `invoice.paid`
|
|
- ✅ `invoice.payment_failed`
|
|
|
|
```typescript
|
|
export async function handleStripeWebhook(req: Request, res: Response, next: NextFunction): Promise<void> {
|
|
try {
|
|
const signature = req.headers['stripe-signature'] as string;
|
|
const event = stripeService.constructWebhookEvent(req.body, signature);
|
|
|
|
switch (event.type) {
|
|
case 'checkout.session.completed': {
|
|
await handleCheckoutComplete(session);
|
|
break;
|
|
}
|
|
// ... more cases
|
|
}
|
|
|
|
res.json({ received: true });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Estado:** ✅ Webhook handler completo y funcional
|
|
|
|
---
|
|
|
|
#### 3. Routes
|
|
|
|
**Archivo:** `apps/backend/src/modules/payments/payments.routes.ts`
|
|
|
|
**Rutas encontradas:**
|
|
- ✅ `POST /payments/webhook` - Stripe webhook endpoint
|
|
- ✅ `POST /payments/checkout` - Create checkout session
|
|
- ✅ `POST /payments/wallet/deposit` - Wallet deposit (usado por DepositForm)
|
|
|
|
**Estado:** ✅ Rutas configuradas correctamente
|
|
|
|
---
|
|
|
|
### Frontend - Análisis de Componentes
|
|
|
|
#### 1. ✅ CORRECTO: DepositForm (PCI-DSS Compliant)
|
|
|
|
**Archivo:** `apps/frontend/src/modules/investment/components/DepositForm.tsx`
|
|
|
|
**Implementación:**
|
|
```typescript
|
|
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
|
|
|
// Renderiza CardElement (NO maneja datos de tarjeta)
|
|
<CardElement options={cardElementOptions} />
|
|
|
|
// Confirma pago con Stripe
|
|
const { error, paymentIntent } = await stripe.confirmCardPayment(
|
|
clientSecret,
|
|
{
|
|
payment_method: {
|
|
card: cardElement, // ← Stripe.js maneja datos
|
|
},
|
|
}
|
|
);
|
|
```
|
|
|
|
**Flujo:**
|
|
1. Usuario completa formulario (monto, cuenta)
|
|
2. Frontend llama `POST /api/v1/payments/wallet/deposit`
|
|
3. Backend crea Payment Intent → devuelve `clientSecret`
|
|
4. Frontend usa `stripe.confirmCardPayment(clientSecret, { payment_method: { card: cardElement } })`
|
|
5. Stripe procesa pago sin que frontend toque datos sensibles
|
|
|
|
**Estado:** ✅ **100% PCI-DSS compliant**
|
|
|
|
**Mensaje en UI (línea 260):**
|
|
> "Your payment is secured by Stripe. We never store your card details."
|
|
|
|
---
|
|
|
|
#### 2. ✅ CORRECTO: StripeElementsWrapper (PCI-DSS Compliant)
|
|
|
|
**Archivo:** `apps/frontend/src/components/payments/StripeElementsWrapper.tsx`
|
|
|
|
**Implementación:**
|
|
```typescript
|
|
import { Elements } from '@stripe/react-stripe-js';
|
|
import { loadStripe } from '@stripe/stripe-js';
|
|
|
|
// Carga Stripe.js
|
|
const stripe = loadStripe(stripeConfig.publicKey, {
|
|
locale: stripeConfig.locale,
|
|
});
|
|
|
|
// Renderiza Elements provider
|
|
<Elements stripe={stripePromise} options={elementsOptions}>
|
|
{children}
|
|
</Elements>
|
|
```
|
|
|
|
**Comentario en código (línea 6):**
|
|
> "IMPORTANT: This component is required for PCI-DSS compliance.
|
|
> All payment forms must be wrapped with this provider."
|
|
|
|
**Features:**
|
|
- Carga Stripe.js desde CDN
|
|
- Configura tema oscuro personalizado
|
|
- Manejo de errores y loading states
|
|
- HOC `withStripeElements()` para wrapping
|
|
|
|
**Estado:** ✅ **Infraestructura correcta para PCI-DSS**
|
|
|
|
---
|
|
|
|
#### 3. ❌ VIOLACIÓN CRÍTICA: PaymentMethodForm
|
|
|
|
**Archivo:** `apps/frontend/src/components/payments/PaymentMethodForm.tsx`
|
|
|
|
**🚨 VIOLACIONES DETECTADAS:**
|
|
|
|
**Violación 1: Estado local almacena datos sensibles (líneas 30-32)**
|
|
```typescript
|
|
const [cardNumber, setCardNumber] = useState(''); // ❌ PAN completo en memoria
|
|
const [expiry, setExpiry] = useState(''); // ❌ Fecha expiración
|
|
const [cvc, setCvc] = useState(''); // ❌ CVV/CVC
|
|
```
|
|
|
|
**Violación 2: Inputs directos para datos de tarjeta (líneas 151-202)**
|
|
```typescript
|
|
<input
|
|
type="text"
|
|
value={cardNumber}
|
|
onChange={(e) => setCardNumber(formatCardNumber(e.target.value))}
|
|
placeholder="1234 5678 9012 3456"
|
|
/>
|
|
```
|
|
|
|
**Violación 3: Envío de datos sensibles al backend (líneas 116-128)**
|
|
```typescript
|
|
const result = await addPaymentMethod({
|
|
type: 'card',
|
|
card: {
|
|
number: cardNumber.replace(/\s/g, ''), // ❌ Envía PAN completo
|
|
exp_month: parseInt(expiry.split('/')[0]), // ❌ Envía mes expiración
|
|
exp_year: 2000 + parseInt(expiry.split('/')[1]), // ❌ Envía año expiración
|
|
cvc, // ❌ Envía CVV
|
|
},
|
|
billing_details: {
|
|
name: cardholderName,
|
|
},
|
|
setAsDefault: makeDefault,
|
|
});
|
|
```
|
|
|
|
**Comentario en código (línea 114):**
|
|
> "In a real implementation, this would use Stripe.js to create a PaymentMethod"
|
|
|
|
**PCI-DSS Requirements Violated:**
|
|
- ❌ Requirement 3: Protect stored cardholder data
|
|
- ❌ Requirement 4: Encrypt transmission of cardholder data
|
|
- ❌ Requirement 6: Develop secure systems and applications
|
|
|
|
**Nivel de Severidad:** 🔴 **CRÍTICO**
|
|
|
|
**Estado de Uso:**
|
|
- ✅ **NO está siendo usado** en ninguna página actualmente
|
|
- Solo exportado en `components/payments/index.ts`
|
|
- Parece ser código legacy/demo
|
|
|
|
---
|
|
|
|
#### 4. ✅ CORRECTO: Billing Page
|
|
|
|
**Archivo:** `apps/frontend/src/modules/payments/pages/Billing.tsx`
|
|
|
|
**Gestión de métodos de pago:**
|
|
- ✅ Usa **Stripe Customer Portal** para agregar métodos de pago (líneas 214-220)
|
|
- ✅ Llama `openBillingPortal()` que redirige a portal hosted por Stripe
|
|
- ✅ NO usa PaymentMethodForm
|
|
|
|
```typescript
|
|
<button onClick={openBillingPortal}>
|
|
<Plus className="w-4 h-4" />
|
|
Agregar
|
|
</button>
|
|
```
|
|
|
|
**Estado:** ✅ **PCI-DSS compliant** (usa portal hosted por Stripe)
|
|
|
|
---
|
|
|
|
#### 5. Frontend payment.service.ts
|
|
|
|
**Archivo:** `apps/frontend/src/services/payment.service.ts`
|
|
|
|
**Problemas encontrados:**
|
|
|
|
**❌ Problema 1: No usa apiClient centralizado (líneas 29-43)**
|
|
```typescript
|
|
// ❌ PROBLEMA: Axios local, no usa apiClient de ST4.1
|
|
const api = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// ❌ Interceptor duplicado
|
|
api.interceptors.request.use((config) => {
|
|
const token = localStorage.getItem('auth_token'); // ❌ Clave diferente: 'auth_token' vs 'token'
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
});
|
|
```
|
|
|
|
**Impacto:**
|
|
- NO se beneficia del auto-refresh de ST4.1
|
|
- Puede causar 401 y logout inesperado
|
|
- Inconsistencia con otros servicios
|
|
|
|
**Prioridad:** ⚠️ ALTA (debe migrarse a apiClient como parte de ST4.1.3)
|
|
|
|
---
|
|
|
|
## A: ANÁLISIS - Gaps y Riesgos
|
|
|
|
### 1. Estado Actual PCI-DSS
|
|
|
|
| Componente | Estado | Compliance |
|
|
|------------|--------|------------|
|
|
| Backend Payment Intents | ✅ Implementado | ✅ Compliant |
|
|
| Backend Webhooks | ✅ Implementado | ✅ Compliant |
|
|
| Frontend DepositForm | ✅ Usa CardElement | ✅ Compliant |
|
|
| Frontend StripeElementsWrapper | ✅ Implementado | ✅ Compliant |
|
|
| Frontend Billing Page | ✅ Usa Customer Portal | ✅ Compliant |
|
|
| **Frontend PaymentMethodForm** | ❌ **Manejo directo datos** | ❌ **VIOLATION** |
|
|
|
|
**Nivel de Riesgo Global:** 🟡 **MEDIO-BAJO**
|
|
|
|
**Razón:**
|
|
- El componente inseguro **NO está en uso** actualmente
|
|
- Los flujos de producción usan implementaciones seguras
|
|
- Riesgo: Developer podría usar PaymentMethodForm sin saber que es inseguro
|
|
|
|
---
|
|
|
|
### 2. Gaps Identificados
|
|
|
|
#### Gap 1: Código Legacy Inseguro
|
|
|
|
**Descripción:** PaymentMethodForm existe pero no cumple PCI-DSS
|
|
|
|
**Impacto:**
|
|
- Riesgo de uso accidental por developers
|
|
- Confusión en código base (2 formas de agregar métodos de pago)
|
|
|
|
**Remediación:**
|
|
- ❌ ELIMINAR PaymentMethodForm.tsx
|
|
- ✅ DOCUMENTAR que debe usarse Stripe Customer Portal o CardElement
|
|
|
|
---
|
|
|
|
#### Gap 2: payment.service.ts no usa apiClient
|
|
|
|
**Descripción:** Usa axios local en lugar de apiClient centralizado de ST4.1
|
|
|
|
**Impacto:**
|
|
- NO se beneficia de auto-refresh tokens
|
|
- Inconsistencia con otros servicios
|
|
- Puede causar 401 inesperados
|
|
|
|
**Remediación:**
|
|
- ⚠️ Migrar a apiClient (parte de ST4.1.3)
|
|
|
|
---
|
|
|
|
#### Gap 3: Falta documentación PCI-DSS
|
|
|
|
**Descripción:** No hay documento técnico que explique arquitectura PCI-DSS
|
|
|
|
**Impacto:**
|
|
- Developers no saben qué componentes usar
|
|
- Riesgo de implementaciones inseguras futuras
|
|
|
|
**Remediación:**
|
|
- 📄 Crear ET-PAY-006: PCI-DSS Architecture & Guidelines
|
|
|
|
---
|
|
|
|
### 3. Arquitectura Actual vs Ideal
|
|
|
|
#### Arquitectura Actual (Flujos en Uso)
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ FLUJO ACTUAL (SEGURO) │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
1. AGREGAR MÉTODO DE PAGO:
|
|
User → Billing Page → openBillingPortal() → Stripe Customer Portal
|
|
↓
|
|
User adds card
|
|
↓
|
|
Webhook → Backend
|
|
↓
|
|
Store PaymentMethod
|
|
|
|
2. DEPOSIT TO WALLET:
|
|
User → DepositForm → <CardElement> → stripe.confirmCardPayment()
|
|
↓ ↓
|
|
(Stripe.js handles) Backend Payment Intent
|
|
↓ ↓
|
|
Success/Error Webhook updates DB
|
|
|
|
✅ AMBOS FLUJOS SON PCI-DSS COMPLIANT
|
|
```
|
|
|
|
#### Código Legacy (No Usado)
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ CÓDIGO LEGACY (INSEGURO - NO USADO) │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
PaymentMethodForm:
|
|
User → <input type="text" cardNumber> → addPaymentMethod()
|
|
↓ ↓
|
|
(PAN en memoria) POST /api con datos raw
|
|
↓ ↓
|
|
❌ VIOLATION ❌ VIOLATION
|
|
|
|
❌ NO DEBE USARSE - ELIMINAR
|
|
```
|
|
|
|
---
|
|
|
|
## P: PLANEACIÓN - Estrategia de Remediación
|
|
|
|
### Opción 1: ELIMINAR Código Inseguro (RECOMENDADA)
|
|
|
|
**Acción:**
|
|
1. Eliminar `PaymentMethodForm.tsx`
|
|
2. Remover export de `components/payments/index.ts`
|
|
3. Documentar en ET-PAY-006 que se debe usar:
|
|
- Stripe Customer Portal (para métodos de pago)
|
|
- CardElement (para pagos one-time)
|
|
|
|
**Ventajas:**
|
|
- ✅ Elimina riesgo de uso accidental
|
|
- ✅ Simplifica codebase
|
|
- ✅ Clarifica arquitectura PCI-DSS
|
|
|
|
**Desventajas:**
|
|
- Ninguna (código no está en uso)
|
|
|
|
**Esfuerzo:** 0.5h
|
|
|
|
---
|
|
|
|
### Opción 2: REFACTOR a Stripe Elements (No Recomendada)
|
|
|
|
**Acción:**
|
|
1. Refactorizar PaymentMethodForm para usar CardElement
|
|
2. Integrar con Payment Intents backend
|
|
3. Mantener ambas opciones (Portal + Form)
|
|
|
|
**Ventajas:**
|
|
- ✅ Permite agregar métodos sin salir de la app
|
|
|
|
**Desventajas:**
|
|
- ❌ Más código a mantener
|
|
- ❌ Stripe Customer Portal ya cubre este caso
|
|
- ❌ Duplicación de funcionalidad
|
|
|
|
**Esfuerzo:** 8h
|
|
|
|
**Decisión:** ❌ NO RECOMENDADA
|
|
|
|
---
|
|
|
|
### Opción 3: MANTENER Status Quo (No Recomendada)
|
|
|
|
**Acción:**
|
|
- No tocar PaymentMethodForm
|
|
- Confiar en que no se usará
|
|
|
|
**Ventajas:**
|
|
- Ninguna
|
|
|
|
**Desventajas:**
|
|
- ❌ Riesgo de uso accidental
|
|
- ❌ Código legacy confunde a developers
|
|
- ❌ No cumple con best practices
|
|
|
|
**Decisión:** ❌ NO RECOMENDADA
|
|
|
|
---
|
|
|
|
## Recomendación Final
|
|
|
|
### ✅ OPCIÓN 1: Eliminar Código Inseguro + Documentar Arquitectura
|
|
|
|
**Justificación:**
|
|
1. PaymentMethodForm **NO está en uso** → Eliminar es seguro
|
|
2. Flujos actuales (Customer Portal + CardElement) **YA cumplen** PCI-DSS
|
|
3. Eliminar código inseguro **previene** riesgos futuros
|
|
4. Documentación clara **guía** a developers
|
|
|
|
**Tareas ST4.2:**
|
|
|
|
| ID | Tarea | Esfuerzo | Prioridad |
|
|
|----|-------|----------|-----------|
|
|
| ST4.2.1 | ❌ ELIMINAR PaymentMethodForm.tsx | 0.25h | P0 |
|
|
| ST4.2.2 | 📄 CREAR ET-PAY-006 PCI-DSS Architecture | 4h | P0 |
|
|
| ST4.2.3 | ✅ Validar flujos actuales (tests) | 8h | P1 |
|
|
| ST4.2.4 | 🔍 Security audit PCI-DSS SAQ-A | 8h | P1 |
|
|
| ST4.2.5 | 📄 ACTUALIZAR developer guidelines | 2h | P2 |
|
|
|
|
**Total Esfuerzo Revisado:** ~22h (en lugar de 80h originales)
|
|
|
|
**Ahorro:** 58h (73% reducción) debido a que backend ya está completo y frontend usa implementaciones seguras
|
|
|
|
---
|
|
|
|
## Próximos Pasos
|
|
|
|
1. ✅ **Aprobar plan** (Opción 1)
|
|
2. 🔄 **Ejecutar ST4.2.1**: Eliminar PaymentMethodForm
|
|
3. 📄 **Ejecutar ST4.2.2**: Crear ET-PAY-006
|
|
4. ✅ **Ejecutar ST4.2.3**: Tests E2E de flujos de pago
|
|
5. 🔍 **Ejecutar ST4.2.4**: Security audit
|
|
6. ✅ **Marcar ST4.2 como COMPLETADO**
|
|
|
|
---
|
|
|
|
**Estado:** ⏳ Pendiente aprobación de usuario
|
|
**Última actualización:** 2026-01-26
|
|
**Autor:** Claude Opus 4.5
|