Segurança
O CSGOFlip implementa múltiplas camadas de segurança para proteger usuários, transações financeiras e a integridade do sistema.
Visão Geral das Camadas
┌─────────────────────────────────────────────────────────────────┐
│ CAMADA 1: REDE │
│ • Cloudflare WAF • DDoS Protection • SSL/TLS │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CAMADA 2: APLICAÇÃO │
│ • Rate Limiting • CORS • Helmet • Input Validation │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CAMADA 3: AUTENTICAÇÃO │
│ • Steam OAuth • Session Management • 2FA (TOTP) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CAMADA 4: AUTORIZAÇÃO │
│ • Guards • Role-Based Access • Resource Ownership │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CAMADA 5: DADOS │
│ • Distributed Locks • Double-Entry • Audit Trail │
└─────────────────────────────────────────────────────────────────┘Autenticação
Steam OAuth 2.0
Não armazenamos senhas. A autenticação é delegada ao Steam:
Gestão de Sessões
Sessões são armazenadas no Redis com TTL de 7 dias:
interface Session {
userId: bigint;
steamId: string;
ip: string;
userAgent: string;
deviceId?: string;
createdAt: Date;
lastActivity: Date;
}
// Estrutura no Redis
// Key: session:{sessionId}
// Value: JSON da sessão
// TTL: 604800 segundos (7 dias)Benefícios:
- ✅ Logout instantâneo (deleta a sessão)
- ✅ Ver todos os dispositivos conectados
- ✅ Logout de todos os dispositivos
- ✅ Detectar sessões suspeitas (IP diferente)
2FA (Two-Factor Authentication)
Para operações sensíveis (saques), exigimos 2FA via TOTP:
// 1. Usuário ativa 2FA
const secret = speakeasy.generateSecret({ length: 20 });
// Retorna QR code para Google Authenticator
// 2. Em cada saque, valida o código
const isValid = speakeasy.totp.verify({
secret: user.twoFactorSecret,
encoding: 'base32',
token: userCode,
window: 1, // Permite 1 código anterior/posterior
});Proteções de Rede
Rate Limiting
Limitamos requisições por IP, usuário e ação:
@Module({
imports: [
ThrottlerModule.forRoot([
{
name: 'short',
ttl: 1000, // 1 segundo
limit: 10, // 10 requisições
},
{
name: 'medium',
ttl: 10000, // 10 segundos
limit: 50, // 50 requisições
},
{
name: 'long',
ttl: 60000, // 1 minuto
limit: 200, // 200 requisições
},
]),
],
})Limites específicos:
| Ação | Limite | Janela |
|---|---|---|
| Login | 5 tentativas | 15 minutos |
| Abertura de caixa | 10/minuto | Por usuário |
| Criar batalha | 5/minuto | Por usuário |
| Depósito | 3/hora | Por usuário |
| Saque | 2/hora | Por usuário |
CORS (Cross-Origin Resource Sharing)
Apenas origens autorizadas podem fazer requisições:
app.enableCors({
origin: [
'https://csgoflip.com',
'https://admin.csgoflip.com',
process.env.NODE_ENV === 'development' && 'http://localhost:3000',
].filter(Boolean),
credentials: true, // Permite cookies
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
});Headers de Segurança
Usando Helmet (adaptado para Fastify):
app.register(helmet, {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Para Next.js
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
},
});Validação de Entrada
ValidationPipe Global
Todas as requisições passam por validação automática:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Remove campos não declarados
forbidNonWhitelisted: true, // Erro se campo extra
transform: true, // Transforma tipos
transformOptions: {
enableImplicitConversion: true,
},
}),
);DTOs com class-validator
export class OpenCaseDto {
@IsNotEmpty()
@IsString()
@Length(1, 32)
caseId: string;
@IsOptional()
@IsInt()
@Min(1)
@Max(10)
quantity?: number = 1;
@IsOptional()
@IsString()
@Length(32, 64)
clientSeed?: string;
}Sanitização
Prevenção contra XSS e injection:
@Injectable()
export class SanitizePipe implements PipeTransform {
transform(value: any) {
if (typeof value === 'string') {
// Remove tags HTML
return value.replace(/<[^>]*>/g, '');
}
return value;
}
}Proteção de Dados Financeiros
Distributed Locks (Redlock)
Previne race conditions em operações de saldo:
async debitBalance(userId: bigint, amount: bigint) {
// Adquire lock exclusivo para este usuário
const lock = await this.redlock.acquire(
[`lock:user:balance:${userId}`],
5000, // 5 segundos de timeout
);
try {
// Operação atômica dentro do lock
const balance = await this.getBalance(userId);
if (balance < amount) {
throw new InsufficientBalanceError();
}
await this.createDebitTransaction(userId, amount);
} finally {
// SEMPRE libera o lock
await lock.release();
}
}Race Condition Prevenida
Sem o lock, duas requisições simultâneas poderiam:
- Ler saldo: R$ 100
- Ambas verificam: R$ 100 >= R$ 80 ✓
- Ambas debitam: R$ 80
- Saldo final: -R$ 60 (ERRO!)
Com lock, a segunda requisição espera a primeira terminar.
Double-Entry Bookkeeping
Toda transação tem débito E crédito:
async transfer(fromUserId: bigint, toUserId: bigint, amount: bigint) {
await this.prisma.$transaction(async (tx) => {
// Cria DEBIT para quem paga
const debit = await tx.transaction.create({
data: {
userId: fromUserId,
type: 'DEBIT',
amountCents: -amount,
reason: 'TRANSFER_OUT',
},
});
// Cria CREDIT para quem recebe
const credit = await tx.transaction.create({
data: {
userId: toUserId,
type: 'CREDIT',
amountCents: amount,
reason: 'TRANSFER_IN',
relatedTransactionId: debit.id, // Liga as transações
},
});
// Atualiza o debit com referência ao credit
await tx.transaction.update({
where: { id: debit.id },
data: { relatedTransactionId: credit.id },
});
});
}Audit Trail Imutável
Todas as ações são logadas com hash encadeado (blockchain-like):
interface AuditLog {
id: bigint;
userId: bigint;
action: string;
entityType: string;
entityId: bigint;
oldData: JsonValue;
newData: JsonValue;
metadata: JsonValue;
previousHash: string; // Hash do registro anterior
currentHash: string; // Hash deste registro
createdAt: Date;
}
// Cálculo do hash
function calculateHash(log: AuditLog, previousHash: string): string {
const data = JSON.stringify({
id: log.id,
userId: log.userId,
action: log.action,
entityType: log.entityType,
entityId: log.entityId,
oldData: log.oldData,
newData: log.newData,
previousHash,
});
return crypto.createHash('sha256').update(data).digest('hex');
}Benefícios:
- ✅ Impossível alterar registros antigos (hash quebraria)
- ✅ Detecta adulteração verificando a cadeia
- ✅ Auditoria completa de todas as ações
Autorização
Guards
// Auth Guard - Verifica sessão válida
@Injectable()
export class AuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const sessionId = request.cookies['sessionId'];
if (!sessionId) {
throw new UnauthorizedException('Session required');
}
const session = await this.sessionService.validate(sessionId);
if (!session) {
throw new UnauthorizedException('Invalid session');
}
request.user = session.user;
return true;
}
}
// Admin Guard - Verifica role de admin
@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
throw new ForbiddenException('Admin access required');
}
return true;
}
}Resource Ownership
Usuários só podem acessar seus próprios recursos:
@Get('inventory')
async getInventory(@CurrentUser() user: User) {
// Usuário só vê SEU inventário
return this.inventoryService.getUserInventory(user.id);
}
@Post('battles/:id/join')
async joinBattle(
@Param('id') battleId: string,
@CurrentUser() user: User,
) {
// Verifica se batalha existe e está aberta
const battle = await this.battleService.findById(battleId);
if (battle.creatorId === user.id) {
throw new BadRequestException('Cannot join your own battle');
}
// Continua...
}Proteções Específicas de Gambling
Provably Fair
Garante que resultados não são manipulados:
// ANTES do jogo
const serverSeed = crypto.randomBytes(32).toString('hex');
const serverSeedHash = crypto.createHash('sha256')
.update(serverSeed)
.digest('hex');
// Envia APENAS o hash para o usuário
// O serverSeed fica secreto
// APÓS o jogo
// Revela o serverSeed para verificação
// Usuário pode calcular: SHA256(serverSeed) === serverSeedHash?Anti-Sniping (Batalhas)
Previne que usuários vejam resultados antes de entrar:
async joinBattle(battleId: bigint, userId: bigint) {
// 1. Lock da batalha
const lock = await this.redlock.acquire([`lock:battle:${battleId}`], 5000);
try {
const battle = await this.battleRepo.findById(battleId);
// 2. Verifica que batalha não começou
if (battle.status !== 'WAITING') {
throw new BadRequestException('Battle already started');
}
// 3. Adiciona jogador ANTES de gerar seeds
await this.battleRepo.addPlayer(battleId, userId);
// 4. Se ficou cheio, gera seeds e inicia
if (battle.players.length === battle.maxPlayers) {
const serverSeed = this.generateServerSeed();
await this.battleRepo.start(battleId, serverSeed);
}
} finally {
await lock.release();
}
}Cooldown de Saque
Previne saques imediatos após depósito (anti-lavagem):
async requestWithdrawal(userId: bigint, amount: bigint) {
const lastDeposit = await this.depositRepo.getLastConfirmed(userId);
if (lastDeposit) {
const hoursSinceDeposit =
(Date.now() - lastDeposit.confirmedAt.getTime()) / (1000 * 60 * 60);
if (hoursSinceDeposit < 24) {
throw new BadRequestException(
'Withdrawals available 24 hours after deposit'
);
}
}
// Continua com saque...
}Monitoramento de Segurança
Alertas Automáticos
// Alerta: Muitas tentativas de login falhadas
if (failedLogins > 10) {
await this.alertService.send({
severity: 'HIGH',
type: 'BRUTE_FORCE_ATTEMPT',
ip: request.ip,
userId: attemptedUserId,
});
}
// Alerta: Saque acima do normal
if (withdrawal.amount > user.avgWithdrawal * 5) {
await this.alertService.send({
severity: 'MEDIUM',
type: 'UNUSUAL_WITHDRAWAL',
userId: user.id,
amount: withdrawal.amount,
});
}
// Alerta: Login de novo IP
if (!knownIps.includes(request.ip)) {
await this.alertService.send({
severity: 'LOW',
type: 'NEW_IP_LOGIN',
userId: user.id,
ip: request.ip,
});
}Logs Estruturados
Todos os logs seguem formato estruturado para análise:
this.logger.log({
event: 'WITHDRAWAL_REQUESTED',
userId: user.id,
amount: amount,
method: 'PIX',
ip: request.ip,
userAgent: request.headers['user-agent'],
timestamp: new Date().toISOString(),
});Checklist de Segurança
Implementado ✅
- [x] Steam OAuth (sem senhas)
- [x] Sessions Redis (revogáveis)
- [x] 2FA para saques
- [x] Rate limiting multi-camada
- [x] CORS whitelist
- [x] Validação de entrada (ValidationPipe)
- [x] Distributed locks (race conditions)
- [x] Double-entry bookkeeping
- [x] Audit trail imutável
- [x] Provably Fair
- [x] Guards de autenticação/autorização
- [x] Headers de segurança (Helmet)
Recomendado para Produção
- [ ] Cloudflare WAF
- [ ] DDoS protection
- [ ] IP reputation checking
- [ ] Device fingerprinting
- [ ] Fraud detection ML
- [ ] PCI DSS compliance (se cartão)
- [ ] Penetration testing regular
Arquivos Fonte Relacionados
Principais Arquivos de Segurança
src/presentation/guards/auth.guard.ts- Guard de autenticaçãosrc/presentation/guards/admin.guard.ts- Guard de adminsrc/infrastructure/auth/session.service.ts- Gestão de sessõessrc/infrastructure/auth/two-factor.service.ts- 2FAsrc/infrastructure/locks/lock.service.ts- Distributed lockssrc/infrastructure/audit/audit.service.ts- Audit trailsrc/application/services/provably-fair.service.ts- Provably Fair
