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