Sistema NEXUS v3.4 migrado con: Estructura principal: - core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles) - core/catalog: Catalogo de funcionalidades reutilizables - shared/knowledge-base: Base de conocimiento compartida - devtools/scripts: Herramientas de desarrollo - control-plane/registries: Control de servicios y CI/CD - orchestration/: Configuracion de orquestacion de agentes Proyectos incluidos (11): - gamilit (submodule -> GitHub) - trading-platform (OrbiquanTIA) - erp-suite con 5 verticales: - erp-core, construccion, vidrio-templado - mecanicas-diesel, retail, clinicas - betting-analytics - inmobiliaria-analytics - platform_marketing_content - pos-micro, erp-basico Configuracion: - .gitignore completo para Node.js/Python/Docker - gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git) - Sistema de puertos estandarizado (3005-3199) Generated with NEXUS v3.4 Migration System EPIC-010: Configuracion Git y Repositorios
779 lines
19 KiB
Markdown
779 lines
19 KiB
Markdown
# 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<string> {
|
|
return bcrypt.hash(password, SALT_ROUNDS);
|
|
}
|
|
|
|
export async function verifyPassword(
|
|
password: string,
|
|
hash: string,
|
|
): Promise<boolean> {
|
|
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<AuthTokens> {
|
|
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<AuthTokens> {
|
|
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<AuthTokens> {
|
|
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<void> {
|
|
// 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<boolean>(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, Permission[]> = {
|
|
[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<Role[]>('roles', [
|
|
context.getHandler(),
|
|
context.getClass(),
|
|
]);
|
|
|
|
const requiredPermissions = this.reflector.getAllAndOverride<Permission[]>(
|
|
'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<Permission>();
|
|
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<UserResponseDto> {
|
|
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
|
|
<div dangerouslySetInnerHTML={{ __html: userInput }} />
|
|
|
|
// ✅ CORRECTO: Sanitizar primero
|
|
import DOMPurify from 'dompurify';
|
|
|
|
<div dangerouslySetInnerHTML={{
|
|
__html: DOMPurify.sanitize(userInput)
|
|
}} />
|
|
|
|
// ✅ MEJOR: Evitar dangerouslySetInnerHTML cuando sea posible
|
|
<div>{userInput}</div> // 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
|