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
19 KiB
19 KiB
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
// 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)
// 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
// ❌ 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// ❌ 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
// 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
// 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
// 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
// ❌ 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
// ❌ 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)
// 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