--- id: "ET-INV-007" title: "Seguridad y Validaciones" type: "Technical Specification" status: "Done" priority: "Alta" epic: "OQI-004" project: "trading-platform" version: "1.0.0" created_date: "2025-12-05" updated_date: "2026-01-04" --- # ET-INV-007: Seguridad y Validaciones **Epic:** OQI-004 Cuentas de Inversión **Versión:** 1.0 **Fecha:** 2025-12-05 **Responsable:** Requirements-Analyst --- ## 1. Descripción Define las medidas de seguridad, validaciones y controles para el módulo de cuentas de inversión: - Autenticación y autorización - Validación de datos de entrada - KYC (Know Your Customer) básico - Límites de transacciones - Prevención de fraude - Auditoría y logging - Encriptación de datos sensibles --- ## 2. Arquitectura de Seguridad ``` ┌─────────────────────────────────────────────────────────────────┐ │ Security Architecture │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Request Layer │ │ │ │ - Rate Limiting │ │ │ │ - CORS │ │ │ │ - Helmet (Security Headers) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Authentication Layer │ │ │ │ - JWT Validation │ │ │ │ - Token Refresh │ │ │ │ - Session Management │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Authorization Layer │ │ │ │ - Role-Based Access Control (RBAC) │ │ │ │ - Resource Ownership Verification │ │ │ │ - Action Permissions │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Validation Layer │ │ │ │ - Input Sanitization │ │ │ │ - Business Rules Validation │ │ │ │ - Amount Limits │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Fraud Detection │ │ │ │ - Suspicious Activity Detection │ │ │ │ - Velocity Checks │ │ │ │ - Anomaly Detection │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Audit & Logging │ │ │ │ - Activity Logs │ │ │ │ - Security Events │ │ │ │ - Compliance Reporting │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## 3. Autenticación y Autorización ### 3.1 Auth Middleware ```typescript // src/middlewares/auth.middleware.ts import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; import { AppError } from '../utils/errors'; import { logger } from '../utils/logger'; interface JwtPayload { user_id: string; email: string; role: string; exp: number; } // Extender Request type declare global { namespace Express { interface Request { user?: { id: string; email: string; role: string; }; } } } /** * Middleware de autenticación JWT */ export const authenticate = async ( req: Request, res: Response, next: NextFunction ): Promise => { try { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { throw new AppError('No authentication token provided', 401); } const token = authHeader.substring(7); const secret = process.env.JWT_SECRET!; // Verificar token const decoded = jwt.verify(token, secret) as JwtPayload; // Adjuntar usuario al request req.user = { id: decoded.user_id, email: decoded.email, role: decoded.role, }; // Log de acceso logger.debug('User authenticated', { user_id: req.user.id, endpoint: req.path, method: req.method, }); next(); } catch (error: any) { if (error.name === 'TokenExpiredError') { logger.warn('Expired token', { path: req.path }); return next(new AppError('Token expired', 401)); } if (error.name === 'JsonWebTokenError') { logger.warn('Invalid token', { path: req.path }); return next(new AppError('Invalid token', 401)); } next(error); } }; /** * Middleware para requerir rol de admin */ export const requireAdmin = ( req: Request, res: Response, next: NextFunction ): void => { if (!req.user) { throw new AppError('Authentication required', 401); } if (req.user.role !== 'admin') { logger.warn('Unauthorized admin access attempt', { user_id: req.user.id, role: req.user.role, path: req.path, }); throw new AppError('Admin access required', 403); } next(); }; /** * Middleware para verificar ownership de recurso */ export const requireOwnership = (resourceType: 'account' | 'withdrawal') => { return async ( req: Request, res: Response, next: NextFunction ): Promise => { try { if (!req.user) { throw new AppError('Authentication required', 401); } const resourceId = req.params.id; // Verificar ownership según tipo de recurso // Esto se debe implementar en el repository const repository = new InvestmentRepository(); let isOwner = false; if (resourceType === 'account') { const account = await repository.getAccountById(resourceId); isOwner = account?.user_id === req.user.id; } else if (resourceType === 'withdrawal') { const withdrawal = await repository.getWithdrawalRequestById(resourceId); isOwner = withdrawal?.user_id === req.user.id; } if (!isOwner && req.user.role !== 'admin') { logger.warn('Unauthorized resource access attempt', { user_id: req.user.id, resource_type: resourceType, resource_id: resourceId, }); throw new AppError('Access denied', 403); } next(); } catch (error) { next(error); } }; }; ``` --- ## 4. Validaciones de Negocio ### 4.1 Investment Validator Service ```typescript // src/services/validation/investment-validator.service.ts import { InvestmentRepository } from '../../modules/investment/investment.repository'; import { AppError } from '../../utils/errors'; import { logger } from '../../utils/logger'; export class InvestmentValidatorService { private repository: InvestmentRepository; constructor() { this.repository = new InvestmentRepository(); } /** * Valida que el usuario pueda crear una cuenta */ async validateAccountCreation(userId: string, productId: string): Promise { // Verificar KYC completado const kycStatus = await this.checkKYCStatus(userId); if (!kycStatus.completed) { throw new AppError('KYC verification required before investing', 400); } // Verificar límite de cuentas por usuario const accountCount = await this.repository.getAccountCountByUser(userId); const MAX_ACCOUNTS = parseInt(process.env.MAX_ACCOUNTS_PER_USER || '10'); if (accountCount >= MAX_ACCOUNTS) { throw new AppError(`Maximum ${MAX_ACCOUNTS} accounts allowed`, 400); } // Verificar que no exista cuenta duplicada const existingAccount = await this.repository.getAccountByUserAndProduct( userId, productId ); if (existingAccount) { throw new AppError('Account already exists for this product', 409); } } /** * Valida un depósito */ async validateDeposit( userId: string, accountId: string, amount: number ): Promise { // Verificar cuenta existe y pertenece al usuario const account = await this.repository.getAccountById(accountId); if (!account) { throw new AppError('Account not found', 404); } if (account.user_id !== userId) { throw new AppError('Access denied', 403); } if (account.status !== 'active') { throw new AppError('Account is not active', 409); } // Verificar monto mínimo const MIN_DEPOSIT = parseFloat(process.env.MIN_DEPOSIT_AMOUNT || '50'); if (amount < MIN_DEPOSIT) { throw new AppError(`Minimum deposit is $${MIN_DEPOSIT}`, 400); } // Verificar monto máximo diario const dailyTotal = await this.repository.getDailyDepositTotal(userId); const MAX_DAILY = parseFloat(process.env.MAX_DAILY_DEPOSIT || '50000'); if (dailyTotal + amount > MAX_DAILY) { throw new AppError(`Daily deposit limit of $${MAX_DAILY} exceeded`, 400); } // Verificar límite del producto const product = await this.repository.getProductById(account.product_id); if (product.max_investment) { const totalInvested = account.total_deposited + amount; if (totalInvested > product.max_investment) { throw new AppError( `Maximum investment for this product is $${product.max_investment}`, 400 ); } } // Velocity check - máximo 5 depósitos por hora const recentDeposits = await this.repository.getRecentDeposits(userId, 3600); if (recentDeposits.length >= 5) { throw new AppError('Too many deposits. Please try again later', 429); } } /** * Valida un retiro */ async validateWithdrawal( userId: string, accountId: string, amount: number ): Promise { // Verificar cuenta const account = await this.repository.getAccountById(accountId); if (!account) { throw new AppError('Account not found', 404); } if (account.user_id !== userId) { throw new AppError('Access denied', 403); } if (account.status !== 'active') { throw new AppError('Account is not active', 409); } // Verificar balance suficiente if (amount > account.current_balance) { throw new AppError('Insufficient balance', 400); } // Verificar monto mínimo de retiro const MIN_WITHDRAWAL = parseFloat(process.env.MIN_WITHDRAWAL_AMOUNT || '50'); if (amount < MIN_WITHDRAWAL) { throw new AppError(`Minimum withdrawal is $${MIN_WITHDRAWAL}`, 400); } // Verificar que no haya solicitud de retiro pendiente const pendingWithdrawal = await this.repository.getPendingWithdrawalByAccount( accountId ); if (pendingWithdrawal) { throw new AppError('There is already a pending withdrawal request', 409); } // Verificar límite diario de retiros const dailyWithdrawals = await this.repository.getDailyWithdrawalTotal(userId); const MAX_DAILY_WITHDRAWAL = parseFloat( process.env.MAX_DAILY_WITHDRAWAL || '25000' ); if (dailyWithdrawals + amount > MAX_DAILY_WITHDRAWAL) { throw new AppError( `Daily withdrawal limit of $${MAX_DAILY_WITHDRAWAL} exceeded`, 400 ); } // Verificar lock period (ej: no retiros en primeros 30 días) const LOCK_PERIOD_DAYS = parseInt(process.env.ACCOUNT_LOCK_PERIOD_DAYS || '0'); if (LOCK_PERIOD_DAYS > 0) { const accountAge = Date.now() - new Date(account.opened_at).getTime(); const lockPeriodMs = LOCK_PERIOD_DAYS * 24 * 60 * 60 * 1000; if (accountAge < lockPeriodMs) { const daysRemaining = Math.ceil( (lockPeriodMs - accountAge) / (24 * 60 * 60 * 1000) ); throw new AppError( `Account is locked for withdrawals. ${daysRemaining} days remaining`, 400 ); } } } /** * Verifica estado de KYC del usuario */ private async checkKYCStatus(userId: string): Promise<{ completed: boolean; level: string; }> { // Implementar integración con servicio de KYC // Por ahora, retornar mock return { completed: true, level: 'basic', }; } /** * Detecta actividad sospechosa */ async detectSuspiciousActivity(userId: string): Promise { // Múltiples cuentas creadas en corto tiempo const recentAccounts = await this.repository.getRecentAccountsByUser( userId, 86400 ); // 24h if (recentAccounts.length >= 3) { logger.warn('Suspicious: Multiple accounts created', { user_id: userId }); return true; } // Patrón de depósito-retiro rápido const recentTransactions = await this.repository.getRecentTransactions( userId, 3600 ); // 1h const hasDepositAndWithdrawal = recentTransactions.some((t) => t.type === 'deposit') && recentTransactions.some((t) => t.type === 'withdrawal'); if (hasDepositAndWithdrawal) { logger.warn('Suspicious: Quick deposit-withdrawal pattern', { user_id: userId, }); return true; } return false; } } ``` --- ## 5. Rate Limiting ### 5.1 Rate Limiter Middleware ```typescript // src/middlewares/rate-limit.middleware.ts import rateLimit from 'express-rate-limit'; import RedisStore from 'rate-limit-redis'; import { createClient } from 'redis'; // Cliente Redis para rate limiting distribuido const redisClient = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379', }); redisClient.connect(); /** * Rate limiter general para API */ export const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutos max: 100, // 100 requests por ventana message: 'Too many requests, please try again later', standardHeaders: true, legacyHeaders: false, store: new RedisStore({ client: redisClient, prefix: 'rl:api:', }), }); /** * Rate limiter estricto para operaciones sensibles */ export const strictLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hora max: 5, // 5 requests por hora message: 'Too many requests for this operation', store: new RedisStore({ client: redisClient, prefix: 'rl:strict:', }), }); /** * Rate limiter personalizado */ export const customRateLimit = (maxRequests: number, windowSeconds: number) => { return rateLimit({ windowMs: windowSeconds * 1000, max: maxRequests, store: new RedisStore({ client: redisClient, prefix: 'rl:custom:', }), }); }; ``` --- ## 6. Encriptación de Datos ### 6.1 Encryption Service ```typescript // src/services/security/encryption.service.ts import crypto from 'crypto'; export class EncryptionService { private algorithm = 'aes-256-gcm'; private key: Buffer; constructor() { const secret = process.env.ENCRYPTION_KEY!; this.key = crypto.scryptSync(secret, 'salt', 32); } /** * Encripta datos sensibles */ encrypt(text: string): string { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(this.algorithm, this.key, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); // Retornar iv:authTag:encrypted return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`; } /** * Desencripta datos */ decrypt(encryptedData: string): string { const parts = encryptedData.split(':'); const iv = Buffer.from(parts[0], 'hex'); const authTag = Buffer.from(parts[1], 'hex'); const encrypted = parts[2]; const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } /** * Hash de datos (one-way) */ hash(data: string): string { return crypto.createHash('sha256').update(data).digest('hex'); } } ``` ### 6.2 Uso en Modelo de Datos ```typescript // Encriptar datos de banco antes de guardar const encryptionService = new EncryptionService(); const destinationDetails = { bank_account: '****1234', routing_number: encryptionService.encrypt('026009593'), account_holder_name: 'John Doe', }; await repository.createWithdrawalRequest({ // ... destination_details: destinationDetails, }); ``` --- ## 7. Auditoría y Logging ### 7.1 Audit Logger ```typescript // src/services/security/audit-logger.service.ts import { logger } from '../../utils/logger'; export enum AuditAction { ACCOUNT_CREATED = 'ACCOUNT_CREATED', DEPOSIT_INITIATED = 'DEPOSIT_INITIATED', DEPOSIT_COMPLETED = 'DEPOSIT_COMPLETED', WITHDRAWAL_REQUESTED = 'WITHDRAWAL_REQUESTED', WITHDRAWAL_APPROVED = 'WITHDRAWAL_APPROVED', WITHDRAWAL_REJECTED = 'WITHDRAWAL_REJECTED', WITHDRAWAL_COMPLETED = 'WITHDRAWAL_COMPLETED', ACCOUNT_PAUSED = 'ACCOUNT_PAUSED', ACCOUNT_CLOSED = 'ACCOUNT_CLOSED', SUSPICIOUS_ACTIVITY = 'SUSPICIOUS_ACTIVITY', } interface AuditLogEntry { action: AuditAction; user_id: string; resource_type: string; resource_id: string; details?: Record; ip_address?: string; user_agent?: string; } export class AuditLoggerService { /** * Registra evento de auditoría */ log(entry: AuditLogEntry): void { logger.info('AUDIT', { timestamp: new Date().toISOString(), action: entry.action, user_id: entry.user_id, resource_type: entry.resource_type, resource_id: entry.resource_id, details: entry.details, ip_address: entry.ip_address, user_agent: entry.user_agent, }); // Opcionalmente, guardar en tabla de auditoría // await auditRepository.create(entry); } /** * Log de evento de seguridad */ logSecurityEvent( event: string, userId: string, severity: 'low' | 'medium' | 'high' | 'critical', details?: Record ): void { logger.warn('SECURITY_EVENT', { timestamp: new Date().toISOString(), event, user_id: userId, severity, details, }); // Si es crítico, enviar alerta if (severity === 'critical') { // await alertService.sendSecurityAlert(event, userId, details); } } } ``` ### 7.2 Uso en Controllers ```typescript // En InvestmentController const auditLogger = new AuditLoggerService(); async createAccount(req: Request, res: Response) { // ... lógica de creación ... auditLogger.log({ action: AuditAction.ACCOUNT_CREATED, user_id: req.user!.id, resource_type: 'account', resource_id: account.id, details: { product_id: data.product_id, initial_investment: data.initial_investment, }, ip_address: req.ip, user_agent: req.headers['user-agent'], }); // ... respuesta ... } ``` --- ## 8. Configuración de Seguridad ### 8.1 Variables de Entorno ```bash # Authentication JWT_SECRET=your-super-secret-jwt-key-change-in-production JWT_EXPIRATION=1d JWT_REFRESH_EXPIRATION=7d # Encryption ENCRYPTION_KEY=your-super-secret-encryption-key-32-chars # Rate Limiting REDIS_URL=redis://localhost:6379 RATE_LIMIT_WINDOW_MS=900000 RATE_LIMIT_MAX_REQUESTS=100 # Business Limits MAX_ACCOUNTS_PER_USER=10 MIN_DEPOSIT_AMOUNT=50.00 MAX_DAILY_DEPOSIT=50000.00 MIN_WITHDRAWAL_AMOUNT=50.00 MAX_DAILY_WITHDRAWAL=25000.00 ACCOUNT_LOCK_PERIOD_DAYS=30 # KYC REQUIRE_KYC_FOR_INVESTMENT=true KYC_SERVICE_URL=https://kyc-service.example.com ``` ### 8.2 Helmet Configuration ```typescript // src/app.ts import helmet from 'helmet'; app.use( helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", 'data:', 'https:'], }, }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true, }, }) ); ``` --- ## 9. Testing de Seguridad ### 9.1 Security Tests ```typescript // tests/security/auth.test.ts describe('Authentication Security', () => { it('should reject requests without token', async () => { const response = await request(app).get('/api/v1/investment/accounts'); expect(response.status).toBe(401); }); it('should reject expired tokens', async () => { const expiredToken = generateExpiredToken(); const response = await request(app) .get('/api/v1/investment/accounts') .set('Authorization', `Bearer ${expiredToken}`); expect(response.status).toBe(401); expect(response.body.error).toContain('expired'); }); it('should reject access to other users accounts', async () => { const user1Token = await getAuthToken('user1'); const user2AccountId = 'account-belongs-to-user2'; const response = await request(app) .get(`/api/v1/investment/accounts/${user2AccountId}`) .set('Authorization', `Bearer ${user1Token}`); expect(response.status).toBe(403); }); }); ``` --- ## 10. Checklist de Seguridad ### 10.1 Pre-Deployment Security Checklist - [ ] Todas las rutas requieren autenticación - [ ] JWT secret es fuerte y único - [ ] Encryption key es de 32 caracteres - [ ] Rate limiting configurado en todos los endpoints - [ ] CORS configurado correctamente - [ ] Helmet habilitado con CSP - [ ] Logs de auditoría funcionando - [ ] Validaciones de input en todos los endpoints - [ ] Ownership verificado en recursos sensibles - [ ] KYC habilitado para nuevas cuentas - [ ] Límites de transacciones configurados - [ ] Datos de banco encriptados - [ ] HTTPS forzado en producción - [ ] Variables de entorno seguras - [ ] Secrets no en código fuente --- ## 11. Referencias - OWASP Top 10 - JWT Best Practices - PCI DSS Compliance - GDPR Data Protection - Express.js Security Best Practices