# PATRON DE SEGURIDAD **Version:** 1.0.0 **Fecha:** 2025-12-08 **Prioridad:** OBLIGATORIA - Seguir en todo el codigo **Sistema:** SIMCO + CAPVED --- ## PROPOSITO Definir patrones de seguridad obligatorios para prevenir vulnerabilidades comunes (OWASP Top 10) y proteger datos sensibles. --- ## 1. OWASP TOP 10 - RESUMEN ``` ╔══════════════════════════════════════════════════════════════════════╗ ║ OWASP TOP 10 - 2021 ║ ╠══════════════════════════════════════════════════════════════════════╣ ║ ║ ║ A01 - Broken Access Control ║ ║ A02 - Cryptographic Failures ║ ║ A03 - Injection ║ ║ A04 - Insecure Design ║ ║ A05 - Security Misconfiguration ║ ║ A06 - Vulnerable Components ║ ║ A07 - Authentication Failures ║ ║ A08 - Software Integrity Failures ║ ║ A09 - Logging & Monitoring Failures ║ ║ A10 - Server-Side Request Forgery (SSRF) ║ ║ ║ ╚══════════════════════════════════════════════════════════════════════╝ ``` --- ## 2. SANITIZACION DE INPUT ### Backend - Validacion con class-validator ```typescript // src/modules/user/dto/create-user.dto.ts import { IsEmail, IsString, MinLength, MaxLength, Matches, IsNotEmpty, } from 'class-validator'; import { Transform } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger'; export class CreateUserDto { @ApiProperty({ example: 'user@example.com' }) @IsEmail({}, { message: 'Email invalido' }) @MaxLength(255) @Transform(({ value }) => value?.toLowerCase().trim()) // Sanitizar email: string; @ApiProperty({ example: 'John' }) @IsString() @IsNotEmpty() @MinLength(2) @MaxLength(100) @Matches(/^[a-zA-ZÀ-ÿ\s'-]+$/, { message: 'Nombre solo puede contener letras', }) @Transform(({ value }) => value?.trim()) // Sanitizar espacios firstName: string; @ApiProperty() @IsString() @MinLength(8) @MaxLength(128) @Matches( /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/, { message: 'Password debe tener mayuscula, minuscula, numero y simbolo' }, ) password: string; } ``` ### Sanitizacion de HTML (Prevenir XSS) ```typescript // src/shared/utils/sanitizer.ts import DOMPurify from 'isomorphic-dompurify'; export function sanitizeHtml(dirty: string): string { return DOMPurify.sanitize(dirty, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'], ALLOWED_ATTR: [], }); } export function stripHtml(dirty: string): string { return DOMPurify.sanitize(dirty, { ALLOWED_TAGS: [], ALLOWED_ATTR: [], }); } // Uso en DTO @Transform(({ value }) => stripHtml(value)) @IsString() comment: string; ``` ### Prevenir SQL Injection ```typescript // ❌ INCORRECTO: SQL Injection vulnerable async findByName(name: string) { return this.repository.query( `SELECT * FROM users WHERE name = '${name}'` // VULNERABLE ); } // ✅ CORRECTO: Usar parametros async findByName(name: string) { return this.repository.query( 'SELECT * FROM users WHERE name = $1', [name], // Parametrizado ); } // ✅ MEJOR: Usar QueryBuilder de TypeORM async findByName(name: string) { return this.repository .createQueryBuilder('user') .where('user.name = :name', { name }) // Automaticamente seguro .getMany(); } ``` --- ## 3. AUTENTICACION ### Password Hashing ```typescript // src/shared/utils/password.util.ts import * as bcrypt from 'bcrypt'; const SALT_ROUNDS = 12; // Minimo 10 para produccion export async function hashPassword(password: string): Promise { return bcrypt.hash(password, SALT_ROUNDS); } export async function verifyPassword( password: string, hash: string, ): Promise { return bcrypt.compare(password, hash); } ``` ### JWT con Refresh Tokens ```typescript // src/modules/auth/services/auth.service.ts @Injectable() export class AuthService { constructor( private readonly jwtService: JwtService, private readonly configService: ConfigService, private readonly userService: UserService, private readonly tokenService: RefreshTokenService, ) {} async login(dto: LoginDto): Promise { const user = await this.validateUser(dto.email, dto.password); if (!user) { // Mensaje generico para no revelar si email existe throw new UnauthorizedException('Credenciales invalidas'); } const tokens = await this.generateTokens(user); // Guardar refresh token hasheado en BD await this.tokenService.saveRefreshToken( user.id, await hashPassword(tokens.refreshToken), ); return tokens; } private async generateTokens(user: UserEntity): Promise { const payload: JwtPayload = { sub: user.id, email: user.email, roles: user.roles.map(r => r.name), }; const [accessToken, refreshToken] = await Promise.all([ this.jwtService.signAsync(payload, { secret: this.configService.get('JWT_SECRET'), expiresIn: '15m', // Corta duracion }), this.jwtService.signAsync( { sub: user.id, type: 'refresh' }, { secret: this.configService.get('JWT_REFRESH_SECRET'), expiresIn: '7d', }, ), ]); return { accessToken, refreshToken }; } async refresh(refreshToken: string): Promise { try { const payload = await this.jwtService.verifyAsync(refreshToken, { secret: this.configService.get('JWT_REFRESH_SECRET'), }); // Verificar que token existe en BD y no fue revocado const storedToken = await this.tokenService.findByUserId(payload.sub); if (!storedToken || !await verifyPassword(refreshToken, storedToken.hash)) { throw new UnauthorizedException('Token invalido'); } const user = await this.userService.findById(payload.sub); return this.generateTokens(user); } catch { throw new UnauthorizedException('Token invalido o expirado'); } } async logout(userId: string): Promise { // Revocar todos los refresh tokens del usuario await this.tokenService.revokeAllUserTokens(userId); } } ``` ### Guard de Autenticacion ```typescript // src/shared/guards/jwt-auth.guard.ts import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { Reflector } from '@nestjs/core'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { constructor(private reflector: Reflector) { super(); } canActivate(context: ExecutionContext) { // Verificar si es ruta publica const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), ]); if (isPublic) { return true; } return super.canActivate(context); } handleRequest(err: any, user: any, info: any) { if (err || !user) { throw err || new UnauthorizedException('No autorizado'); } return user; } } ``` --- ## 4. AUTORIZACION (RBAC) ### Roles y Permisos ```typescript // src/shared/enums/roles.enum.ts export enum Role { SUPER_ADMIN = 'super_admin', ADMIN = 'admin', MANAGER = 'manager', USER = 'user', GUEST = 'guest', } export enum Permission { // Users USER_CREATE = 'user:create', USER_READ = 'user:read', USER_UPDATE = 'user:update', USER_DELETE = 'user:delete', // Products PRODUCT_CREATE = 'product:create', PRODUCT_READ = 'product:read', PRODUCT_UPDATE = 'product:update', PRODUCT_DELETE = 'product:delete', } // Mapeo de roles a permisos export const ROLE_PERMISSIONS: Record = { [Role.SUPER_ADMIN]: Object.values(Permission), [Role.ADMIN]: [ Permission.USER_CREATE, Permission.USER_READ, Permission.USER_UPDATE, Permission.PRODUCT_CREATE, Permission.PRODUCT_READ, Permission.PRODUCT_UPDATE, Permission.PRODUCT_DELETE, ], [Role.MANAGER]: [ Permission.USER_READ, Permission.PRODUCT_CREATE, Permission.PRODUCT_READ, Permission.PRODUCT_UPDATE, ], [Role.USER]: [ Permission.PRODUCT_READ, ], [Role.GUEST]: [], }; ``` ### Guard de Roles ```typescript // src/shared/guards/roles.guard.ts import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { Role, Permission, ROLE_PERMISSIONS } from '../enums/roles.enum'; @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride('roles', [ context.getHandler(), context.getClass(), ]); const requiredPermissions = this.reflector.getAllAndOverride( 'permissions', [context.getHandler(), context.getClass()], ); if (!requiredRoles && !requiredPermissions) { return true; // Sin restricciones } const { user } = context.switchToHttp().getRequest(); if (!user) { throw new ForbiddenException('Usuario no autenticado'); } // Verificar roles if (requiredRoles?.length > 0) { const hasRole = requiredRoles.some(role => user.roles?.includes(role)); if (!hasRole) { throw new ForbiddenException('Rol insuficiente'); } } // Verificar permisos if (requiredPermissions?.length > 0) { const userPermissions = this.getUserPermissions(user.roles); const hasPermission = requiredPermissions.every( permission => userPermissions.includes(permission), ); if (!hasPermission) { throw new ForbiddenException('Permiso insuficiente'); } } return true; } private getUserPermissions(roles: Role[]): Permission[] { const permissions = new Set(); for (const role of roles) { for (const permission of ROLE_PERMISSIONS[role] || []) { permissions.add(permission); } } return Array.from(permissions); } } ``` ### Decoradores ```typescript // src/shared/decorators/roles.decorator.ts import { SetMetadata } from '@nestjs/common'; import { Role, Permission } from '../enums/roles.enum'; export const Roles = (...roles: Role[]) => SetMetadata('roles', roles); export const Permissions = (...permissions: Permission[]) => SetMetadata('permissions', permissions); ``` ### Uso en Controller ```typescript // src/modules/user/controllers/user.controller.ts @Controller('users') @UseGuards(JwtAuthGuard, RolesGuard) export class UserController { @Get() @Roles(Role.ADMIN, Role.MANAGER) findAll() { return this.userService.findAll(); } @Post() @Permissions(Permission.USER_CREATE) create(@Body() dto: CreateUserDto) { return this.userService.create(dto); } @Delete(':id') @Roles(Role.SUPER_ADMIN) // Solo super admin puede eliminar remove(@Param('id') id: string) { return this.userService.remove(id); } } ``` --- ## 5. PROTECCION DE DATOS ### Encriptacion de Datos Sensibles ```typescript // src/shared/utils/encryption.util.ts import * as crypto from 'crypto'; const ALGORITHM = 'aes-256-gcm'; const IV_LENGTH = 16; const AUTH_TAG_LENGTH = 16; export function encrypt(text: string, key: string): string { const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv( ALGORITHM, Buffer.from(key, 'hex'), iv, ); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); // IV + AuthTag + Encrypted return iv.toString('hex') + authTag.toString('hex') + encrypted; } export function decrypt(encryptedText: string, key: string): string { const iv = Buffer.from(encryptedText.slice(0, IV_LENGTH * 2), 'hex'); const authTag = Buffer.from( encryptedText.slice(IV_LENGTH * 2, (IV_LENGTH + AUTH_TAG_LENGTH) * 2), 'hex', ); const encrypted = encryptedText.slice((IV_LENGTH + AUTH_TAG_LENGTH) * 2); const decipher = crypto.createDecipheriv( ALGORITHM, Buffer.from(key, 'hex'), iv, ); decipher.setAuthTag(authTag); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } ``` ### Columnas Encriptadas en Entity ```typescript // src/shared/transformers/encrypted.transformer.ts import { ValueTransformer } from 'typeorm'; import { encrypt, decrypt } from '../utils/encryption.util'; export class EncryptedTransformer implements ValueTransformer { constructor(private readonly key: string) {} to(value: string | null): string | null { if (!value) return null; return encrypt(value, this.key); } from(value: string | null): string | null { if (!value) return null; return decrypt(value, this.key); } } // Uso en Entity @Column({ type: 'text', transformer: new EncryptedTransformer(process.env.ENCRYPTION_KEY), }) ssn: string; // Se guarda encriptado en BD ``` ### Nunca Exponer Datos Sensibles ```typescript // ❌ INCORRECTO: Exponer password en response @Get(':id') async findOne(@Param('id') id: string) { return this.userRepository.findOne({ where: { id } }); // Retorna { id, email, password, ... } - PASSWORD EXPUESTO! } // ✅ CORRECTO: Usar ResponseDto que excluye campos sensibles @Get(':id') async findOne(@Param('id') id: string): Promise { const user = await this.userService.findOne(id); return plainToClass(UserResponseDto, user, { excludeExtraneousValues: true, }); } // ResponseDto solo expone campos seguros export class UserResponseDto { @Expose() id: string; @Expose() email: string; @Expose() firstName: string; // password NO esta expuesto } ``` --- ## 6. RATE LIMITING ### Implementacion con Throttler ```typescript // src/app.module.ts import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; @Module({ imports: [ ThrottlerModule.forRoot([ { name: 'short', ttl: 1000, // 1 segundo limit: 3, // 3 requests por segundo }, { name: 'medium', ttl: 10000, // 10 segundos limit: 20, // 20 requests por 10 segundos }, { name: 'long', ttl: 60000, // 1 minuto limit: 100, // 100 requests por minuto }, ]), ], providers: [ { provide: APP_GUARD, useClass: ThrottlerGuard, }, ], }) export class AppModule {} ``` ### Rate Limiting por Endpoint ```typescript // Rate limit especifico para login (prevenir brute force) @Post('login') @Throttle({ default: { limit: 5, ttl: 60000 } }) // 5 intentos por minuto async login(@Body() dto: LoginDto) { return this.authService.login(dto); } // Endpoint sin rate limit @Get('health') @SkipThrottle() healthCheck() { return { status: 'ok' }; } ``` --- ## 7. HEADERS DE SEGURIDAD ### Helmet Middleware ```typescript // src/main.ts import helmet from 'helmet'; async function bootstrap() { const app = await NestFactory.create(AppModule); // Headers de seguridad app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", 'data:', 'https:'], }, }, hsts: { maxAge: 31536000, // 1 año includeSubDomains: true, }, })); // CORS configurado app.enableCors({ origin: process.env.CORS_ORIGINS?.split(',') || false, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], credentials: true, }); await app.listen(3000); } ``` --- ## 8. FRONTEND - SEGURIDAD ### Almacenamiento de Tokens ```typescript // ❌ INCORRECTO: Token en localStorage (vulnerable a XSS) localStorage.setItem('token', accessToken); // ✅ MEJOR: HttpOnly cookies (configurado desde backend) // El token se maneja automaticamente por el navegador // ✅ ALTERNATIVA: Si debe estar en JS, usar memoria class TokenStore { private accessToken: string | null = null; setToken(token: string) { this.accessToken = token; } getToken(): string | null { return this.accessToken; } clearToken() { this.accessToken = null; } } export const tokenStore = new TokenStore(); ``` ### Prevenir XSS en React ```typescript // ❌ INCORRECTO: dangerouslySetInnerHTML sin sanitizar
// ✅ CORRECTO: Sanitizar primero import DOMPurify from 'dompurify';
// ✅ MEJOR: Evitar dangerouslySetInnerHTML cuando sea posible
{userInput}
// React escapa automaticamente ``` ### Validacion en Frontend (Defense in Depth) ```typescript // src/shared/schemas/user.schema.ts import { z } from 'zod'; export const createUserSchema = z.object({ email: z.string() .email('Email invalido') .max(255) .transform(v => v.toLowerCase().trim()), firstName: z.string() .min(2, 'Minimo 2 caracteres') .max(100) .regex(/^[a-zA-ZÀ-ÿ\s'-]+$/, 'Solo letras permitidas') .transform(v => v.trim()), password: z.string() .min(8, 'Minimo 8 caracteres') .max(128) .regex( /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/, 'Debe incluir mayuscula, minuscula, numero y simbolo', ), }); ``` --- ## 9. CHECKLIST DE SEGURIDAD ``` Input/Output: [ ] Todos los inputs validados con class-validator [ ] HTML sanitizado antes de renderizar [ ] SQL usa queries parametrizadas [ ] Datos sensibles nunca en logs [ ] ResponseDto excluye campos sensibles Autenticacion: [ ] Passwords hasheados con bcrypt (rounds >= 10) [ ] JWT con expiracion corta (< 15min) [ ] Refresh tokens almacenados hasheados [ ] Logout revoca tokens [ ] Mensajes de error genericos (no revelar info) Autorizacion: [ ] Guards en todos los endpoints protegidos [ ] Verificacion de ownership en recursos [ ] Roles y permisos implementados [ ] Principio de minimo privilegio Infraestructura: [ ] HTTPS obligatorio [ ] Headers de seguridad (Helmet) [ ] CORS configurado correctamente [ ] Rate limiting implementado [ ] Secrets en variables de entorno Frontend: [ ] No localStorage para tokens sensibles [ ] CSP configurado [ ] Validacion client-side (defense in depth) [ ] No exponer errores detallados a usuarios ``` --- ## 10. RECURSOS ADICIONALES - OWASP Cheat Sheets: https://cheatsheetseries.owasp.org/ - NestJS Security: https://docs.nestjs.com/security/helmet - React Security: https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml --- **Version:** 1.0.0 | **Sistema:** SIMCO | **Tipo:** Patron de Seguridad