Changes include: - Updated architecture documentation - Enhanced module definitions (OQI-001 to OQI-008) - ML integration documentation updates - Trading strategies documentation - Orchestration and inventory updates - Docker configuration updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
528 lines
14 KiB
Markdown
528 lines
14 KiB
Markdown
---
|
|
id: "ET-PAY-006"
|
|
title: "Seguridad PCI DSS"
|
|
type: "Technical Specification"
|
|
status: "Done"
|
|
priority: "Alta"
|
|
epic: "OQI-005"
|
|
project: "trading-platform"
|
|
version: "1.0.0"
|
|
created_date: "2025-12-05"
|
|
updated_date: "2026-01-04"
|
|
---
|
|
|
|
# ET-PAY-006: Seguridad PCI DSS
|
|
|
|
**Epic:** OQI-005 Pagos y Stripe
|
|
**Versión:** 1.0
|
|
**Fecha:** 2025-12-05
|
|
|
|
---
|
|
|
|
## 1. Descripción
|
|
|
|
Implementación de medidas de seguridad para cumplimiento PCI DSS:
|
|
- Tokenización de tarjetas con Stripe
|
|
- No almacenamiento de datos sensibles
|
|
- Validaciones de seguridad
|
|
- Logs de auditoría
|
|
- Encriptación de datos
|
|
- Prevención de fraude
|
|
|
|
---
|
|
|
|
## 2. Arquitectura de Seguridad
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Payment Security Stack │
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ Frontend Backend Stripe │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Stripe.js │───►│ Tokenization │───►│ Vault │ │
|
|
│ │ (PCI SAQ-A) │ │ Only Tokens │ │ (Card Data) │ │
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
│ │
|
|
│ ┌──────────────┐ │
|
|
│ │ Fraud │ │
|
|
│ │ Detection │ │
|
|
│ └──────────────┘ │
|
|
│ │
|
|
│ ┌──────────────┐ │
|
|
│ │ Audit Logs │ │
|
|
│ └──────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 3. PCI DSS Requirements
|
|
|
|
### 3.1 Nivel de Cumplimiento
|
|
|
|
**SAQ-A (Self-Assessment Questionnaire A)**
|
|
|
|
OrbiQuant califica para SAQ-A porque:
|
|
- No almacena, procesa ni transmite datos de tarjetas
|
|
- Usa Stripe.js y Elements (redirección completa a Stripe)
|
|
- Solo maneja tokens de Stripe, no datos de tarjetas
|
|
|
|
### 3.2 Requisitos SAQ-A
|
|
|
|
1. Usar solo proveedores PCI DSS compliant (Stripe)
|
|
2. No almacenar datos sensibles (CVV, track data, PIN)
|
|
3. Mantener política de seguridad
|
|
4. Usar conexiones seguras (HTTPS)
|
|
5. No usar contraseñas por defecto
|
|
|
|
---
|
|
|
|
## 4. Implementación de Seguridad
|
|
|
|
### 4.1 Tokenization Service
|
|
|
|
```typescript
|
|
// src/services/security/tokenization.service.ts
|
|
|
|
import { StripeService } from '../stripe/stripe.service';
|
|
import { AppError } from '../../utils/errors';
|
|
|
|
export class TokenizationService {
|
|
private stripeService: StripeService;
|
|
|
|
constructor() {
|
|
this.stripeService = new StripeService();
|
|
}
|
|
|
|
/**
|
|
* NUNCA acepta datos de tarjeta directamente
|
|
* Solo acepta tokens de Stripe
|
|
*/
|
|
async validatePaymentToken(token: string): Promise<boolean> {
|
|
// Verificar que es un token válido de Stripe
|
|
if (!token.startsWith('pm_') && !token.startsWith('tok_')) {
|
|
throw new AppError('Invalid payment token format', 400);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Guardar payment method (solo token)
|
|
*/
|
|
async savePaymentMethod(params: {
|
|
user_id: string;
|
|
payment_method_id: string; // Token de Stripe, NO datos de tarjeta
|
|
customer_id: string;
|
|
}): Promise<void> {
|
|
// Validar token
|
|
await this.validatePaymentToken(params.payment_method_id);
|
|
|
|
// Adjuntar a customer en Stripe
|
|
await this.stripeService.attachPaymentMethod(
|
|
params.payment_method_id,
|
|
params.customer_id
|
|
);
|
|
|
|
// Guardar solo metadata en DB (NO datos de tarjeta)
|
|
// Ver ET-PAY-001 para estructura de tabla payment_methods
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4.2 Data Validation
|
|
|
|
```typescript
|
|
// src/middlewares/payment-validation.middleware.ts
|
|
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import { AppError } from '../utils/errors';
|
|
|
|
/**
|
|
* Valida que NO se envíen datos sensibles de tarjetas
|
|
*/
|
|
export const preventCardDataSubmission = (
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction
|
|
): void => {
|
|
const sensitiveFields = [
|
|
'card_number',
|
|
'cvv',
|
|
'cvc',
|
|
'card_cvv',
|
|
'expiry',
|
|
'exp_month',
|
|
'exp_year',
|
|
];
|
|
|
|
const body = JSON.stringify(req.body).toLowerCase();
|
|
|
|
for (const field of sensitiveFields) {
|
|
if (body.includes(field)) {
|
|
throw new AppError(
|
|
'Card data not accepted. Use Stripe tokenization.',
|
|
400
|
|
);
|
|
}
|
|
}
|
|
|
|
next();
|
|
};
|
|
```
|
|
|
|
### 4.3 Fraud Detection
|
|
|
|
```typescript
|
|
// src/services/security/fraud-detection.service.ts
|
|
|
|
import { PaymentRepository } from '../../modules/payments/payment.repository';
|
|
import { logger } from '../../utils/logger';
|
|
|
|
export class FraudDetectionService {
|
|
private paymentRepo: PaymentRepository;
|
|
|
|
constructor() {
|
|
this.paymentRepo = new PaymentRepository();
|
|
}
|
|
|
|
/**
|
|
* Detecta actividad sospechosa de pagos
|
|
*/
|
|
async detectFraud(params: {
|
|
user_id: string;
|
|
amount: number;
|
|
ip_address?: string;
|
|
}): Promise<{ is_suspicious: boolean; reasons: string[] }> {
|
|
const reasons: string[] = [];
|
|
|
|
// 1. Verificar múltiples pagos fallidos
|
|
const failedPayments = await this.paymentRepo.getRecentFailedPayments(
|
|
params.user_id,
|
|
3600 // última hora
|
|
);
|
|
|
|
if (failedPayments.length >= 3) {
|
|
reasons.push('Multiple failed payment attempts');
|
|
}
|
|
|
|
// 2. Verificar monto inusualmente alto
|
|
const avgPayment = await this.paymentRepo.getAveragePaymentAmount(params.user_id);
|
|
|
|
if (avgPayment > 0 && params.amount > avgPayment * 10) {
|
|
reasons.push('Unusually high payment amount');
|
|
}
|
|
|
|
// 3. Velocity check - múltiples pagos en corto tiempo
|
|
const recentPayments = await this.paymentRepo.getRecentPayments(
|
|
params.user_id,
|
|
1800 // últimos 30 min
|
|
);
|
|
|
|
if (recentPayments.length >= 5) {
|
|
reasons.push('Too many payments in short time');
|
|
}
|
|
|
|
// 4. Verificar cambios frecuentes de payment method
|
|
const recentMethods = await this.paymentRepo.getRecentPaymentMethodChanges(
|
|
params.user_id,
|
|
86400 // último día
|
|
);
|
|
|
|
if (recentMethods.length >= 3) {
|
|
reasons.push('Frequent payment method changes');
|
|
}
|
|
|
|
const is_suspicious = reasons.length > 0;
|
|
|
|
if (is_suspicious) {
|
|
logger.warn('Suspicious payment activity detected', {
|
|
user_id: params.user_id,
|
|
reasons,
|
|
amount: params.amount,
|
|
});
|
|
}
|
|
|
|
return { is_suspicious, reasons };
|
|
}
|
|
|
|
/**
|
|
* Verifica si usuario está en lista de bloqueo
|
|
*/
|
|
async isBlocked(userId: string): Promise<boolean> {
|
|
// Implementar lógica de lista negra
|
|
// Puede usar Redis o tabla en DB
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Bloquea usuario temporalmente
|
|
*/
|
|
async blockUser(userId: string, reason: string, durationSeconds: number): Promise<void> {
|
|
logger.warn('User blocked from payments', {
|
|
user_id: userId,
|
|
reason,
|
|
duration: durationSeconds,
|
|
});
|
|
|
|
// Guardar en Redis con TTL
|
|
// await redis.setex(`blocked:${userId}`, durationSeconds, reason);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4.4 Audit Logger
|
|
|
|
```typescript
|
|
// src/services/security/payment-audit.service.ts
|
|
|
|
import { logger } from '../../utils/logger';
|
|
|
|
export enum PaymentAuditAction {
|
|
PAYMENT_INITIATED = 'PAYMENT_INITIATED',
|
|
PAYMENT_COMPLETED = 'PAYMENT_COMPLETED',
|
|
PAYMENT_FAILED = 'PAYMENT_FAILED',
|
|
REFUND_REQUESTED = 'REFUND_REQUESTED',
|
|
REFUND_COMPLETED = 'REFUND_COMPLETED',
|
|
SUBSCRIPTION_CREATED = 'SUBSCRIPTION_CREATED',
|
|
SUBSCRIPTION_CANCELED = 'SUBSCRIPTION_CANCELED',
|
|
PAYMENT_METHOD_ADDED = 'PAYMENT_METHOD_ADDED',
|
|
PAYMENT_METHOD_REMOVED = 'PAYMENT_METHOD_REMOVED',
|
|
FRAUD_DETECTED = 'FRAUD_DETECTED',
|
|
}
|
|
|
|
interface PaymentAuditEntry {
|
|
action: PaymentAuditAction;
|
|
user_id: string;
|
|
amount?: number;
|
|
payment_id?: string;
|
|
ip_address?: string;
|
|
user_agent?: string;
|
|
metadata?: Record<string, any>;
|
|
}
|
|
|
|
export class PaymentAuditService {
|
|
log(entry: PaymentAuditEntry): void {
|
|
logger.info('PAYMENT_AUDIT', {
|
|
timestamp: new Date().toISOString(),
|
|
action: entry.action,
|
|
user_id: entry.user_id,
|
|
amount: entry.amount,
|
|
payment_id: entry.payment_id,
|
|
ip_address: entry.ip_address,
|
|
user_agent: entry.user_agent,
|
|
metadata: entry.metadata,
|
|
});
|
|
|
|
// Opcionalmente guardar en tabla de auditoría
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Frontend Security
|
|
|
|
### 5.1 Stripe.js Integration
|
|
|
|
```typescript
|
|
// CORRECTO: Usar Stripe Elements
|
|
import { CardElement } from '@stripe/react-stripe-js';
|
|
|
|
const PaymentForm = () => {
|
|
const stripe = useStripe();
|
|
const elements = useElements();
|
|
|
|
const handleSubmit = async () => {
|
|
const cardElement = elements.getElement(CardElement);
|
|
|
|
// Crear token con Stripe.js (datos nunca pasan por nuestro servidor)
|
|
const { token, error } = await stripe.createToken(cardElement);
|
|
|
|
if (token) {
|
|
// Enviar solo token al backend
|
|
await api.post('/payments', { token: token.id });
|
|
}
|
|
};
|
|
|
|
return <CardElement />;
|
|
};
|
|
```
|
|
|
|
```typescript
|
|
// INCORRECTO: NUNCA hacer esto
|
|
const BadPaymentForm = () => {
|
|
const [cardNumber, setCardNumber] = useState('');
|
|
const [cvv, setCvv] = useState('');
|
|
|
|
// ❌ NUNCA capturar datos de tarjeta directamente
|
|
return (
|
|
<form>
|
|
<input
|
|
type="text"
|
|
value={cardNumber}
|
|
onChange={(e) => setCardNumber(e.target.value)}
|
|
/>
|
|
<input
|
|
type="text"
|
|
value={cvv}
|
|
onChange={(e) => setCvv(e.target.value)}
|
|
/>
|
|
</form>
|
|
);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Security Checklist
|
|
|
|
### 6.1 PCI DSS Compliance Checklist
|
|
|
|
- [ ] Usar solo Stripe.js/Elements para captura de tarjetas
|
|
- [ ] NUNCA almacenar CVV/CVC
|
|
- [ ] NUNCA almacenar datos completos de tarjeta
|
|
- [ ] Solo guardar tokens de Stripe
|
|
- [ ] Usar HTTPS en todos los endpoints
|
|
- [ ] Validar firma de webhooks de Stripe
|
|
- [ ] Implementar rate limiting
|
|
- [ ] Logs de auditoría para todas las transacciones
|
|
- [ ] Detección de fraude básica
|
|
- [ ] Encriptación de datos en tránsito (TLS 1.2+)
|
|
- [ ] Acceso restringido a datos de pagos (RBAC)
|
|
- [ ] Monitoreo de actividad sospechosa
|
|
- [ ] Política de contraseñas fuertes
|
|
- [ ] Autenticación de dos factores para admin
|
|
|
|
### 6.2 Development Checklist
|
|
|
|
- [ ] Variables de entorno seguras
|
|
- [ ] Secrets no en código fuente
|
|
- [ ] Test mode keys para desarrollo
|
|
- [ ] Production keys solo en producción
|
|
- [ ] Webhook signatures verificadas
|
|
- [ ] Error messages sin información sensible
|
|
- [ ] Input validation en todos los endpoints
|
|
- [ ] XSS protection
|
|
- [ ] CSRF protection
|
|
- [ ] SQL injection prevention
|
|
|
|
---
|
|
|
|
## 7. Incident Response
|
|
|
|
### 7.1 Procedimiento de Incidente
|
|
|
|
1. **Detección**
|
|
- Monitoreo de logs
|
|
- Alertas automáticas
|
|
- Reportes de usuarios
|
|
|
|
2. **Contención**
|
|
- Bloquear usuario afectado
|
|
- Pausar procesos automáticos
|
|
- Aislar sistemas comprometidos
|
|
|
|
3. **Investigación**
|
|
- Analizar logs de auditoría
|
|
- Identificar alcance
|
|
- Documentar hallazgos
|
|
|
|
4. **Recuperación**
|
|
- Revertir cambios no autorizados
|
|
- Restaurar desde backup si necesario
|
|
- Verificar integridad de datos
|
|
|
|
5. **Post-Mortem**
|
|
- Documentar incidente
|
|
- Implementar mejoras
|
|
- Actualizar procedimientos
|
|
|
|
---
|
|
|
|
## 8. Monitoring y Alertas
|
|
|
|
### 8.1 Métricas Clave
|
|
|
|
```typescript
|
|
// Alertas automáticas
|
|
const ALERT_THRESHOLDS = {
|
|
FAILED_PAYMENTS_PER_HOUR: 10,
|
|
HIGH_VALUE_TRANSACTION: 10000,
|
|
REFUND_RATE_PERCENTAGE: 5,
|
|
WEBHOOK_FAILURE_RATE: 0.1,
|
|
};
|
|
|
|
// Monitorear
|
|
- Tasa de pagos fallidos
|
|
- Volumen de reembolsos
|
|
- Tiempo de respuesta de Stripe
|
|
- Errores de webhook
|
|
- Intentos de fraude detectados
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Configuración
|
|
|
|
```bash
|
|
# Security
|
|
STRIPE_SECRET_KEY=sk_live_... # Nunca sk_test_ en producción
|
|
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
|
|
# Encryption
|
|
ENCRYPTION_KEY=32-character-secure-key
|
|
|
|
# Rate Limiting
|
|
PAYMENT_RATE_LIMIT_PER_HOUR=10
|
|
REFUND_RATE_LIMIT_PER_DAY=3
|
|
|
|
# Fraud Detection
|
|
FRAUD_DETECTION_ENABLED=true
|
|
MAX_PAYMENT_AMOUNT=10000
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Testing
|
|
|
|
```typescript
|
|
// tests/security/pci-compliance.test.ts
|
|
|
|
describe('PCI Compliance', () => {
|
|
it('should reject card data in request body', async () => {
|
|
const response = await request(app)
|
|
.post('/api/v1/payments')
|
|
.send({
|
|
card_number: '4242424242424242',
|
|
cvv: '123',
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toContain('tokenization');
|
|
});
|
|
|
|
it('should only accept Stripe tokens', async () => {
|
|
const response = await request(app)
|
|
.post('/api/v1/payments')
|
|
.send({
|
|
payment_method_id: 'pm_1234567890',
|
|
amount: 100,
|
|
});
|
|
|
|
expect(response.status).not.toBe(400);
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Referencias
|
|
|
|
- [PCI DSS SAQ-A](https://www.pcisecuritystandards.org/document_library)
|
|
- [Stripe Security](https://stripe.com/docs/security/stripe)
|
|
- [OWASP Payment Security](https://owasp.org/www-project-payment-security/)
|
|
- [PCI Compliance Guide](https://stripe.com/guides/pci-compliance)
|