Este documento aborda todas as práticas de segurança implementadas no projeto Varion, incluindo autenticação, autorização, proteção de dados, segurança de API e conformidade com padrões de segurança.
- Múltiplas camadas de segurança
- Validação em frontend e backend
- Monitoramento contínuo
- Usuários têm apenas permissões necessárias
- Tokens com escopo limitado
- Acesso baseado em roles
- Negação por padrão
- Configurações seguras por default
- Logs detalhados de segurança
// backend/src/utils/auth.ts import jwt from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; import { User } from '../entities/User'; export interface JWTPayload { userId: string; email: string; role: string; iat: number; exp: number; } export class AuthService { private readonly JWT_SECRET = process.env.JWT_SECRET!; private readonly JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d'; private readonly REFRESH_SECRET = process.env.REFRESH_SECRET!; async generateTokens(user: User) { const payload = { userId: user.id, email: user.email, role: user.role, }; const accessToken = jwt.sign(payload, this.JWT_SECRET, { expiresIn: '15m', algorithm: 'HS256', }); const refreshToken = jwt.sign(payload, this.REFRESH_SECRET, { expiresIn: '7d', algorithm: 'HS256', }); return { accessToken, refreshToken }; } async verifyToken(token: string): Promise<JWTPayload> { try { return jwt.verify(token, this.JWT_SECRET) as JWTPayload; } catch (error) { throw new Error('Token inválido'); } } async hashPassword(password: string): Promise<string> { const saltRounds = 12; return bcrypt.hash(password, saltRounds); } async comparePassword(password: string, hash: string): Promise<boolean> { return bcrypt.compare(password, hash); } }
// backend/src/middlewares/auth.middleware.ts import { Request, Response, NextFunction } from 'express'; import { AuthService } from '../utils/auth'; export interface AuthenticatedRequest extends Request { user?: { userId: string; email: string; role: string; }; } export const authMiddleware = async ( req: AuthenticatedRequest, res: Response, next: NextFunction ) => { try { const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).json({ success: false, error: 'Token de acesso requerido', }); } const token = authHeader.split(' ')[1]; // Bearer <token> if (!token) { return res.status(401).json({ success: false, error: 'Formato de token inválido', }); } const authService = new AuthService(); const payload = await authService.verifyToken(token); req.user = { userId: payload.userId, email: payload.email, role: payload.role, }; next(); } catch (error) { return res.status(401).json({ success: false, error: 'Token inválido ou expirado', }); } };
// backend/src/middlewares/role.middleware.ts import { Response, NextFunction } from 'express'; import { AuthenticatedRequest } from './auth.middleware'; export enum UserRole { ADMIN = 'admin', USER = 'user', VIEWER = 'viewer', } export const roleMiddleware = (allowedRoles: UserRole[]) => { return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { const userRole = req.user?.role as UserRole; if (!userRole || !allowedRoles.includes(userRole)) { return res.status(403).json({ success: false, error: 'Acesso negado: permissões insuficientes', }); } next(); }; }; // Uso em rotas // router.delete('/projects/:id', authMiddleware, roleMiddleware([UserRole.ADMIN]), deleteProject);
// backend/src/schemas/user.schema.ts import { z } from 'zod'; export const createUserSchema = z.object({ email: z .string() .email('Email inválido') .max(255, 'Email muito longo'), password: z .string() .min(8, 'Senha deve ter pelo menos 8 caracteres') .max(128, 'Senha muito longa') .regex( /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, 'Senha deve conter pelo menos: 1 minúscula, 1 maiúscula, 1 número e 1 caractere especial' ), name: z .string() .min(2, 'Nome deve ter pelo menos 2 caracteres') .max(100, 'Nome muito longo') .regex(/^[a-zA-ZÀ-ÿ\s]+$/, 'Nome deve conter apenas letras e espaços'), }); export const loginSchema = z.object({ email: z.string().email('Email inválido'), password: z.string().min(1, 'Senha é obrigatória'), }); export type CreateUserInput = z.infer<typeof createUserSchema>; export type LoginInput = z.infer<typeof loginSchema>;
// backend/src/middlewares/validation.middleware.ts import { Request, Response, NextFunction } from 'express'; import { ZodSchema } from 'zod'; export const validateBody = (schema: ZodSchema) => { return (req: Request, res: Response, next: NextFunction) => { try { schema.parse(req.body); next(); } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ success: false, error: 'Dados inválidos', details: error.errors.map(err => ({ field: err.path.join('.'), message: err.message, })), }); } next(error); } }; };
// backend/src/utils/sanitizer.ts import DOMPurify from 'dompurify'; import { JSDOM } from 'jsdom'; const window = new JSDOM('').window; const purify = DOMPurify(window); export class Sanitizer { static sanitizeHtml(dirty: string): string { return purify.sanitize(dirty, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'], ALLOWED_ATTR: [], }); } static sanitizeInput(input: string): string { return input .trim() .replace(/[<>\"']/g, '') // Remove caracteres perigosos .substring(0, 1000); // Limite de tamanho } static sanitizeObject(obj: Record<string, any>): Record<string, any> { const sanitized: Record<string, any> = {}; for (const [key, value] of Object.entries(obj)) { if (typeof value === 'string') { sanitized[key] = this.sanitizeInput(value); } else if (typeof value === 'object' && value !== null) { sanitized[key] = this.sanitizeObject(value); } else { sanitized[key] = value; } } return sanitized; } }
// backend/src/middlewares/rate-limit.middleware.ts import rateLimit from 'express-rate-limit'; import { Request } from 'express'; // Rate limiter geral export const generalLimiter = rateLimit({ windowMs: 15 * 60 * 1000 // 15 minutos max: 100, // Máximo 100 requests por IP message: { success: false, error: 'Muitas requisições. Tente novamente em 15 minutos.', }, standardHeaders: true, legacyHeaders: false, }); // Rate limiter para login export const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutos max: 5, // Máximo 5 tentativas de login skipSuccessfulRequests: true, keyGenerator: (req: Request) => { return req.ip + ':' + req.body.email; }, message: { success: false, error: 'Muitas tentativas de login. Tente novamente em 15 minutos.', }, }); // Rate limiter para criação de recursos export const createLimiter = rateLimit({ windowMs: 60 * 1000, // 1 minuto max: 10, // Máximo 10 criações por minuto message: { success: false, error: 'Limite de criação excedido. Aguarde 1 minuto.', }, });
// backend/src/middlewares/csrf.middleware.ts import csrf from 'csurf'; export const csrfProtection = csrf({ cookie: { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', }, });
// backend/src/middlewares/xss.middleware.ts import helmet from 'helmet'; export const xssProtection = helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'"], fontSrc: ["'self'"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"], }, }, crossOriginEmbedderPolicy: false, });
// backend/src/utils/query-builder.ts import { Repository, SelectQueryBuilder } from 'typeorm'; export class SafeQueryBuilder<T> { constructor(private repository: Repository<T>) {} createSafeQuery(): SelectQueryBuilder<T> { return this.repository.createQueryBuilder(); } // Método seguro para adicionar condições WHERE addWhereCondition( query: SelectQueryBuilder<T>, field: string, value: any, operator: '=' | 'LIKE' | 'IN' | '>' | '<' = '=' ): SelectQueryBuilder<T> { const paramName = `param_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; switch (operator) { case 'LIKE': return query.andWhere(`${field} LIKE :${paramName}`, { [paramName]: `%${value}%`, }); case 'IN': return query.andWhere(`${field} IN (:...${paramName})`, { [paramName]: Array.isArray(value) ? value : [value], }); default: return query.andWhere(`${field} ${operator} :${paramName}`, { [paramName]: value, }); } } } // Uso const queryBuilder = new SafeQueryBuilder(projectRepository); const query = queryBuilder .createSafeQuery() .select(['project.id', 'project.name']) .addWhereCondition('project.name', searchTerm, 'LIKE') .addWhereCondition('project.status', 'active', '=');
// backend/src/utils/encryption.ts import crypto from 'crypto'; export class EncryptionService { private readonly algorithm = 'aes-256-gcm'; private readonly key = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex'); encrypt(text: string): string { const iv = crypto.randomBytes(16); const cipher = crypto.createCipher(this.algorithm, this.key); cipher.setAAD(Buffer.from('varion-app', 'utf8')); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted; } decrypt(encryptedData: string): string { const [ivHex, authTagHex, encrypted] = encryptedData.split(':'); const iv = Buffer.from(ivHex, 'hex'); const authTag = Buffer.from(authTagHex, 'hex'); const decipher = crypto.createDecipher(this.algorithm, this.key); decipher.setAAD(Buffer.from('varion-app', 'utf8')); decipher.setAuthTag(authTag); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } hashSensitiveData(data: string): string { return crypto .createHmac('sha256', this.key) .update(data) .digest('hex'); } }
// backend/src/entities/User.ts import { Entity, Column, BeforeInsert, BeforeUpdate } from 'typeorm'; import { EncryptionService } from '../utils/encryption'; @Entity('users') export class User { @Column({ type: 'varchar', length: 255 }) email: string; @Column({ type: 'varchar', length: 255 }) private encryptedPhone?: string; @Column({ type: 'varchar', length: 255 }) private encryptedDocument?: string; @Column({ type: 'timestamp', nullable: true }) consentDate?: Date; @Column({ type: 'boolean', default: false }) marketingConsent: boolean; @Column({ type: 'timestamp', nullable: true }) dataRetentionDate?: Date; private encryptionService = new EncryptionService(); // Getters e Setters para dados sensíveis get phone(): string | undefined { return this.encryptedPhone ? this.encryptionService.decrypt(this.encryptedPhone) : undefined; } set phone(value: string | undefined) { this.encryptedPhone = value ? this.encryptionService.encrypt(value) : undefined; } get document(): string | undefined { return this.encryptedDocument ? this.encryptionService.decrypt(this.encryptedDocument) : undefined; } set document(value: string | undefined) { this.encryptedDocument = value ? this.encryptionService.encrypt(value) : undefined; } @BeforeInsert() @BeforeUpdate() updateDataRetention() { // Definir data de retenção (ex: 2 anos após último acesso) this.dataRetentionDate = new Date(); this.dataRetentionDate.setFullYear(this.dataRetentionDate.getFullYear() + 2); } // Método para anonimizar dados anonymize() { this.email = `anonymous_${this.id}@deleted.local`; this.encryptedPhone = undefined; this.encryptedDocument = undefined; this.marketingConsent = false; } }
// backend/src/config/security.ts import helmet from 'helmet'; import cors from 'cors'; export const securityMiddleware = [ helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], fontSrc: ["'self'", "https://fonts.gstatic.com"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'", process.env.FRONTEND_URL || "http://localhost:3000"], }, }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true, }, }), cors({ origin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'], credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], }), ];
// backend/src/routes/v1/index.ts import express from 'express'; import { authMiddleware } from '../../middlewares/auth.middleware'; import { generalLimiter } from '../../middlewares/rate-limit.middleware'; const router = express.Router(); // Aplicar middlewares de segurança para toda a API v1 router.use(generalLimiter); router.use('/auth', require('./auth.routes')); router.use('/projects', authMiddleware, require('./projects.routes')); router.use('/states', authMiddleware, require('./states.routes')); export default router;
// backend/src/utils/security-logger.ts import winston from 'winston'; export class SecurityLogger { private logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'logs/security.log', level: 'warn' }), new winston.transports.File({ filename: 'logs/audit.log' }), ], }); logFailedLogin(email: string, ip: string, userAgent: string) { this.logger.warn('Failed login attempt', { event: 'FAILED_LOGIN', email, ip, userAgent, timestamp: new Date().toISOString(), }); } logSuccessfulLogin(userId: string, email: string, ip: string) { this.logger.info('Successful login', { event: 'SUCCESSFUL_LOGIN', userId, email, ip, timestamp: new Date().toISOString(), }); } logSuspiciousActivity(userId: string, activity: string, details: any) { this.logger.warn('Suspicious activity detected', { event: 'SUSPICIOUS_ACTIVITY', userId, activity, details, timestamp: new Date().toISOString(), }); } logDataAccess(userId: string, resource: string, action: string) { this.logger.info('Data access', { event: 'DATA_ACCESS', userId, resource, action, timestamp: new Date().toISOString(), }); } }
// backend/src/utils/anomaly-detector.ts import { SecurityLogger } from './security-logger'; export class AnomalyDetector { private securityLogger = new SecurityLogger(); private userActivity = new Map<string, any>(); detectAnomalies(userId: string, activity: any) { const userHistory = this.userActivity.get(userId) || { locations: [], timePatterns: [], actionCounts: {}, }; // Detectar login de localização incomum if (activity.type === 'login') { const isUnusualLocation = this.checkUnusualLocation( userHistory.locations, activity.location ); if (isUnusualLocation) { this.securityLogger.logSuspiciousActivity( userId, 'Unusual location login', { location: activity.location, previousLocations: userHistory.locations } ); } } // Detectar atividade excessiva if (activity.type === 'api_call') { const actionCount = userHistory.actionCounts[activity.endpoint] || 0; if (actionCount > 100) { // Threshold configurável this.securityLogger.logSuspiciousActivity( userId, 'Excessive API calls', { endpoint: activity.endpoint, count: actionCount } ); } } this.updateUserActivity(userId, activity); } private checkUnusualLocation(previousLocations: string[], currentLocation: string): boolean { // Implementar lógica de detecção de localização incomum return !previousLocations.includes(currentLocation); } private updateUserActivity(userId: string, activity: any) { // Atualizar histórico de atividade do usuário const userHistory = this.userActivity.get(userId) || { locations: [], timePatterns: [], actionCounts: {}, }; if (activity.type === 'login') { userHistory.locations.push(activity.location); } if (activity.type === 'api_call') { userHistory.actionCounts[activity.endpoint] = (userHistory.actionCounts[activity.endpoint] || 0) + 1; } this.userActivity.set(userId, userHistory); } }
// frontend/src/utils/api-client.ts import axios, { AxiosRequestConfig } from 'axios'; const apiClient = axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, timeout: 10000, withCredentials: true, }); // Request interceptor para adicionar token apiClient.interceptors.request.use( (config) => { const token = localStorage.getItem('accessToken'); if (token) { config.headers.Authorization = `Bearer ${token}`; } // Adicionar CSRF token se disponível const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); if (csrfToken) { config.headers['X-CSRF-Token'] = csrfToken; } return config; }, (error) => Promise.reject(error) ); // Response interceptor para lidar com erros de autenticação apiClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { // Token expirado, redirecionar para login localStorage.removeItem('accessToken'); window.location.href = '/login'; } return Promise.reject(error); } ); export default apiClient;
// frontend/next.config.ts const nextConfig = { async headers() { return [ { source: '/(.*)', headers: [ { key: 'X-Frame-Options', value: 'DENY', }, { key: 'X-Content-Type-Options', value: 'nosniff', }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin', }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()', }, ], }, ]; }, }; export default nextConfig;
# .env.example # Banco de Dados DATABASE_URL=postgresql://user:password@localhost:5432/varion DB_ENCRYPTION_KEY=32-byte-hex-key-for-data-encryption # Autenticação JWT_SECRET=very-long-random-string-at-least-256-bits REFRESH_SECRET=another-very-long-random-string JWT_EXPIRES_IN=15m REFRESH_EXPIRES_IN=7d # Criptografia ENCRYPTION_KEY=32-byte-hex-key-for-general-encryption SALT_ROUNDS=12 # Rate Limiting RATE_LIMIT_WINDOW=900000 RATE_LIMIT_MAX=100 LOGIN_RATE_LIMIT_MAX=5 # CORS CORS_ORIGIN=https://yourdomain.com,https://www.yourdomain.com FRONTEND_URL=https://yourdomain.com # Logs LOG_LEVEL=info SECURITY_LOG_FILE=logs/security.log AUDIT_LOG_FILE=logs/audit.log # Feature Flags ENABLE_RATE_LIMITING=true ENABLE_CSRF_PROTECTION=true ENABLE_ANOMALY_DETECTION=true
# backend/Dockerfile FROM node:18-alpine # Criar usuário não-root RUN addgroup -g 1001 -S nodejs RUN adduser -S nodejs -u 1001 # Definir diretório de trabalho WORKDIR /app # Copiar arquivos de dependências COPY package*.json ./ COPY pnpm-lock.yaml ./ # Instalar dependências RUN npm install -g pnpm RUN pnpm install --frozen-lockfile --only=production # Copiar código da aplicação COPY --chown=nodejs:nodejs . . # Remover arquivos desnecessários RUN rm -rf .git .env.example README.md # Mudar para usuário não-root USER nodejs # Expor porta EXPOSE 3001 # Comando de inicialização CMD ["node", "dist/server.js"]
- Autenticação JWT implementada
- Rate limiting configurado
- Validação de entrada com Zod
- Sanitização de dados
- Headers de segurança (Helmet)
- CORS configurado adequadamente
- Logs de segurança implementados
- Criptografia de dados sensíveis
- SQL injection prevention
- XSS protection
- CSRF protection
- HTTPS enforced
- CSP headers configurados
- Sanitização de dados de entrada
- Secure token storage
- Logout automático em inatividade
- Validação client-side
- Error handling seguro
- Firewall configurado
- SSL/TLS certificates
- Backup encryption
- Database security
- Container security
- Network segmentation
- Monitoring e alertas
- LGPD/GDPR compliance
- Data retention policies
- Consent management
- Audit logging
- Vulnerability scanning
- Security testing
- Incident response plan
- Detecção: Monitoramento contínuo e alertas
- Contenção: Isolamento imediato da ameaça
- Erradicação: Remoção da causa raiz
- Recuperação: Restauração dos serviços
- Lições Aprendidas: Melhoria dos processos
- Security Team: security@varion.com
- DevOps Team: devops@varion.com
- Management: management@varion.com
- SIEM: Splunk/ELK Stack
- Vulnerability Scanner: OWASP ZAP
- Dependency Check: Snyk
- Container Security: Trivy