Geek Social — Documentação
Conceitos

Security

Modelo de ameaças e medidas de segurança implementadas + checklist OWASP.

Resumo defensivo do Geek Social. Lista o que está coberto, o que não está, e o que está no roadmap.

Modelo de ameaças

Atores considerados:

  • User regular — pode tentar acessar/manipular dados de outros users via API
  • User malicioso autenticado — tem JWT válido mas tenta exploits
  • Atacante externo (não autenticado) — força bruta, enumeration, scans
  • Atacante com acesso físico ao DB — vazamento de secrets em rest
  • Compromise de credencial — JWT/cookie roubado

NÃO considerados (out of scope hoje):

  • Atacante com root no servidor (compromise infra) — fora do escopo da app
  • Análise sofisticada de side-channels (timing, cache)

Autenticação e sessão

VetorMitigação
Sessão hijack via XSSRefresh token em cookie HttpOnly — JS não lê
CSRF em refreshSameSite=strict em prod, lax em dev
Replay de refreshRotação obrigatória — usar 2x falha
Roubo de DB → forjar sessõestoken_hash = sha256(rawToken) — DB nunca tem raw
Brute force de senhabcrypt cost 12 (~250ms) + (TODO) rate limit em /login
Enumeration de e-mails/forgot-password sempre retorna 200
Reset com token roubadoSingle-use (used_at) + TTL 1h
Múltiplas sessões abertasReset password apaga TODOS refresh_tokens
JWT roubado dentro de 15minAceito risco residual; reset password é rota de recuperação

Detalhes em Conceito de Autenticação.

Autorização (vertical)

Cada endpoint que mexe com recurso valida ownership/membership:

// Padrão em services
async update(userId: string, listingId: string, input: ...) {
  const listing = await this.repo.findById(listingId)
  if (!listing) throw new ListingsError('NOT_FOUND')
  if (listing.ownerId !== userId) throw new ListingsError('NOT_AUTHORIZED')
  // ... prossegue
}

Princípio: validação centrada no service (não no controller). Controllers podem ser ignorados pra debugging mas service nunca abre brecha.

RecursoOwnership/access check
User profile (edit)userId === request.user.userId
Collection (CRUD)collection.userId === userId
Item (CRUD)via collection ownership
Listing (CRUD)listing.ownerId === userId
Offer (accept/reject)turno via latestProposal.proposerId !== userId
Group chat (admin actions)member.role IN ('owner', 'admin')
Notification (mark read)notification.recipientId === userId

Validação de input

Todas as rotas têm Zod schema:

  • Body: body: registerSchema no app.post(...)
  • Query: querystring: ...
  • Params: params: ...

fastify-type-provider-zod valida antes do handler rodar. Schema inválido → 400 sem chegar no service.

Tipos com validação extra:

  • UUIDs: z.string().uuid() rejeita strings malformadas
  • Emails: z.string().email() rejeita formato inválido
  • URLs: z.string().url() rejeita protocolos suspeitos
  • Hex colors: regex ^#[0-9a-fA-F]{6}$
  • Paths/filenames de upload: multipart valida via mime + size limit

SQL injection

Não há SQL string concatenation no código de domínio.

Drizzle parameteriza:

  • db.select().from(users).where(eq(users.email, userInput)) → bind parameter
  • sql`SELECT ... ${userInput}` → bind parameter (não inline)

Cuidado: sql.raw(userInput) injeta direto. Nunca usado no código atual. Ponto de atenção em PRs.

XSS

Frontend (Vue 3): escape automático via interpolação {{ value }}. v-html proibido em conteúdo de usuário.

Backend: APIs retornam JSON. Sem renderização server-side. Markdown não é renderizado em bio/posts (texto plano).

Vetores possíveis (quando renderizar markdown no futuro):

  • Sanitizar HTML antes de renderizar (DOMPurify)
  • Whitelist de tags permitidas
  • Atributos de URL com target="_blank" rel="noopener noreferrer"

Path traversal em uploads

S3Adapter recebe key montado pelo backend:

const key = `posts/${postId}/${randomUUID()}.${ext}`
await s3.put(key, buffer, mimeType)

postId é UUID validado. randomUUID() é seguro. ext extraído do filename é único ponto sensível:

const ext = path.extname(filename).toLowerCase()
if (!['.jpg', '.png', '.webp', '.mp4', '.m4a', '.pdf'].includes(ext)) {
  throw new Error('UNSUPPORTED_EXT')
}

Whitelist evita extensões executáveis (.html, .js, .svg que pode ter JS).

Storage público

Arquivos no MinIO/S3 são publicamente acessíveis via URL. Implicação:

CenárioRisco
Post privateQuem souber a URL acessa o arquivo
Message attachment em conversa privadaIdem
Avatar de user com perfil privateIdem

Mitigação atual: URLs incluem UUID random, então não é enumerável. Mas leak de URL = leak de mídia.

Mitigação futura (signed URLs): backend gera URL temporária (TTL minutos) por request. Custo: complexidade adicional, URLs não-cacheáveis.

Rate limiting

Implementado via middleware customizado em api/src/shared/middleware/rate-limit.ts com store Redis (auditoria #5, Pacote B).

// rate-limit.ts — INCR + PEXPIRE atômicos por janela fixa
const key = `rl:ip:${ip}:${Math.floor(Date.now() / windowMs)}`
const count = await redis.multi().incr(key).pexpire(key, windowMs).exec()
if (count > maxRequests) throw 429
  • Sem fallback in-memory: rate-limit é controle de segurança. Se o Redis cair, o middleware fail-closes com 503 RATE_LIMIT_UNAVAILABLE (forçando o operador a tratar a indisponibilidade) em vez de silenciosamente liberar tudo.
  • Distribuído: funciona com N réplicas da API — o Redis é a fonte da verdade do contador.
  • Algoritmo: janela fixa (fixed window counter) — aceita até 2× limit no instante do rollover; suficiente pros endpoints atuais.

Endpoints com limite hoje:

EndpointLimiteEscopo
/auth/register10/minIP
/auth/login5/minIP
/auth/forgot-password5/minIP
/auth/reset-password5/minIP
/auth/resend-verification5/minIP
/auth/verify-email20/minIP
/auth/set-password3/horauserId
/auth/change-password5/horauserId
/auth/google/exchange5/minIP
/users/search30/minuserId
DELETE /users/me1/24huserId
/notifications (deleteAll)5/horauserId
/notifications/:id (delete)20/horauserId

Secrets handling

SecretOnde ficaCuidado
JWT_SECRETenv varRandom ≥32 chars; rotação requer re-login de todos
GOOGLE_CLIENT_SECRETenv var
AWS_SECRET_ACCESS_KEYenv var
VAPID_PRIVATE_KEYenv varTrocar invalida todas push subscriptions
users.steam_api_keyDBAt-rest sem criptografia (pendência)
password_hashDBbcrypt cost 12
refresh_tokens.token_hashDBsha256
password_reset_tokens.token_hashDBsha256

.env no .gitignore. Em prod: secrets via env do container (não no Dockerfile).

CORS

await app.register(fastifyCors, {
  origin: env.FRONTEND_URL,
  credentials: true,
})

Single origin (frontend específico). Cookies credentials: true exige origem exata, não wildcard.

CSRF

Token Bearer + cookie HttpOnly:

  • Bearer JWT em request normal — não vai automaticamente, atacante precisa do JS pra forjar
  • Cookie refresh — SameSite=strict impede cross-site

Atacante que consegue XSS no nosso domínio passa por tudo. Defesa profundidade contra XSS é prioridade.

Headers de segurança

Implementado via @fastify/helmet registrado em app.ts após o CORS (auditoria #4, Pacote B).

await app.register(fastifyHelmet, {
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'none'"],
      baseUri: ["'self'"],
      frameAncestors: ["'none'"],
      formAction: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", 'data:'],
      connectSrc: ["'self'"],
      fontSrc: ["'self'", 'data:'],
      objectSrc: ["'none'"],
    },
  },
  crossOriginResourcePolicy: { policy: 'cross-origin' },
  hsts: env.NODE_ENV === 'production'
    ? { maxAge: 31536000, includeSubDomains: true, preload: true }
    : false,
})
HeaderValor
Strict-Transport-Security1 ano + includeSubDomains + preload (apenas em prod)
Content-Security-Policydefault-src 'none' + sources mínimas
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY (via frame-ancestors 'none')
Referrer-Policyno-referrer
Cross-Origin-Resource-Policycross-origin (necessário pra frontend separado)
Cross-Origin-Embedder-Policydesabilitado (incompatível com mídia pública S3)

A API serve apenas JSON (e Swagger em dev), por isso o CSP pode ser bem restritivo. Pra docs/ferramentas que precisem de CSP relaxado, use o frontend ou um subdomínio dedicado.

Trust proxy

TRUST_PROXY env var (auditoria #3, Pacote B) controla como o Fastify lê request.ip atrás de proxies/load balancers:

ValorUso
false (default dev)request.ip = socket peer (correto sem proxy)
trueconfia no X-Forwarded-For da request (only-prod-com-proxy-confiavel)
Númeronº de hops a confiar
CIDR(s)lista separada por vírgula (ex: 10.0.0.0/8,192.168.0.0/16)

Por que importa pro rate-limit: sem TRUST_PROXY configurado, todas as requests atrás de um proxy reverso aparecem com o IP do proxy → rate-limit no request.ip viraria limit global por IP do load balancer. Com TRUST_PROXY correto, o IP real do cliente é extraído.

Criptografia end-to-end (chat)

O chat (DMs e grupos) é end-to-end encrypted com Signal Protocol — o backend só vê ciphertext + material público de chave. Nenhuma instância do servidor (incluindo operadores com acesso ao DB) consegue ler conteúdo.

ComponenteAlgoritmo
Identidade long-termCurve25519 (32B pub/priv)
Pre-key signed (SPK)Curve25519 + assinatura Ed25519, rotação ~7d
Post-quantum prekeyKyber1024 (PQXDH), rotação ~7d
One-time prekeyCurve25519, lote 100, refill quando <20
DMX3DH/PQXDH + Double Ratchet (HKDF-SHA256 + AES-256-GCM)
GruposSender Keys distribuídas via Signal session 1-1
Backup de identidadePBKDF2-SHA256(PIN, 600k iter) → AES-256-GCM, blob até 256 KB

Garantias:

  • Forward secrecy: comprometer uma message key não decifra outras mensagens.
  • Post-quantum: Kyber1024 na composição PQXDH protege contra "harvest now, decrypt later" mesmo se Curve25519 cair pra computação quântica.
  • Group forward secrecy: quando membro sai/é removido, conversations.sender_key_id rotaciona — ex-membro não decifra mensagens futuras.
  • TOFU + safety numbers: identidade do peer cacheada localmente, troca silenciosa dispara alerta + bloqueia até user acknowledgar.
  • Push notifications metadata-only: server VAPID nunca vê plaintext.

Detalhes completos em Criptografia E2EE.

Logging vs privacidade

Logs não devem conter:

  • Senhas (em qualquer forma)
  • JWTs completos (apenas claims relevantes: userId)
  • Cookies inteiros
  • E-mails personalizados em mass logs (LGPD)

Pino sanitiza por default; mas hooks customizados podem vazar — auditar antes de centralizar logs.

Compliance LGPD (princípios)

  • Direito ao esquecimento: DELETE /users/me apaga conta + cascade — implementado
  • Portabilidade: endpoint pra exportar dados próprios (GET /users/me/export) — pendência
  • Consentimento explícito: termos no signup — UI implementada
  • Dados sensíveis (CPF, geo precisa, etc.) — não coletados hoje

OWASP Top 10 — checklist

#CategoriaStatus
A01Broken Access Control✅ Validação por service em todos recursos
A02Cryptographic Failures✅ bcrypt, sha256, HTTPS em prod, E2EE Signal Protocol no chat
A03Injection (SQLi/CMD)✅ Drizzle parametriza; Zod valida
A04Insecure Design⚠️ Storage público é decisão consciente
A05Security Misconfiguration✅ Helmet/CSP/HSTS configurados (Pacote B)
A06Vulnerable Components⚠️ npm audit periódico; deps RCE corrigidas no Pacote A; sem Dependabot ainda
A07Identification & Auth Failures✅ JWT + rotation + rate-limit Redis em endpoints sensíveis
A08Software & Data Integrity⚠️ Sem signed builds / SBOM
A09Security Logging & Monitoring❌ Audit log pendente; alerts pendente
A10SSRF⚠️ Sem requests outbound a URLs controladas pelo user; Steam OpenID validado por allowlist (pendente — Pacote C)

Itens em ⚠️ ou ❌ são parte do roadmap. O processo de auditoria interna está dividido em pacotes A–E (round 1 — 2026-04-30):

PacoteItensStatus
ABump deps com RCE (happy-dom, serialize-javascript) + bloqueio de secrets dev-only em prod✅ commit 579e206
BtrustProxy + helmet/CSP + rate-limit Redis distribuído✅ commit 0a4b498
CSSRF, Steam OpenID allowlist, IDOR no chat, Google delete confirm + P2 auth🔜
Dscheme guard, IndexedDB encryption, OAuth fragment, EventEdit guard, logout cleanup🔜
ECSP frontend, withCredentials, magic bytes, body limit + deps P2 (esbuild, vite, uuid)🔜

Pendências priorizadas (pós-Pacote B)

  1. Pacote C — SSRF / OpenID allowlist / IDOR chat
  2. Pacote D — IDB encryption (cripto material no IndexedDB) + logout cleanup
  3. Pacote E — frontend CSP + magic bytes em uploads
  4. Audit log dedicado
  5. Signed URLs pra storage privado
  6. Criptografia at-rest de steam_api_key em DB
  7. npm audit em CI pra alertar de CVE em deps

On this page