Files
lgcosta 6e0ec29888 fix(security): corrige achados da varredura — render, brute-force, upload, WYSIWYG, cookie, sessões
1. /api/render: cap de 512 KiB na entrada (amplificação ~2× medida);
   decodeJSON agora amarra http.MaxBytesReader ao ResponseWriter (413
   real ao cliente em todos os endpoints JSON)
2. Login com rate-limit por IP em memória (5 falhas/15 min → 429, acerto
   reseta, RemoteAddr puro — X-F-F não é confiável) + log de falha com IP
3. Upload: MaxBytesReader no corpo inteiro (64 MiB + 64 KiB) antes do
   parse do multipart; quota global de 2 GiB (MaxFilesTotalBytes) em AddFile
4. sanitizeRich neutraliza href/src com javascript:/vbscript:/data:
   (case/whitespace-insensível); link legítimo preservado
5. Cookie Secure passa a ser configuração explícita (NOTES_SECURE_COOKIE)
   — X-Forwarded-Proto é spoofável; documentado em .env.example e compose
6. Cookie agora carrega id de sessão opaco (crypto/rand 32B, TTL 30d,
   revogação no logout e no restart); token mestre só vale via Bearer —
   furo pego pelo próprio teste novo (token no cookie era aceito)
   Testes: rate-limit, revogação, canais separados, render 413, upload cap
   sw v16
2026-11-05 18:58:10 -03:00

74 lines
1.7 KiB
Go

package api
import (
"crypto/rand"
"encoding/hex"
"sync"
"time"
)
// sessionStore mantém sessões de login em memória: o cookie do navegador
// carrega um id de sessão opaco — nunca o token mestre. Reiniciar o processo
// revoga tudo (o usuário faz login de novo); logout individual revoga na hora.
// Não há crypto própria: id vem de crypto/rand.
type sessionStore struct {
mu sync.Mutex
m map[string]time.Time // id → expiração
ttl time.Duration
}
func newSessionStore(ttl time.Duration) *sessionStore {
return &sessionStore{m: map[string]time.Time{}, ttl: ttl}
}
// new cria uma sessão e devolve (id, expiração).
func (ss *sessionStore) new() (string, time.Time) {
var buf [32]byte
if _, err := rand.Read(buf[:]); err != nil {
// crypto/rand falhar é condição fatal de sistema; não há fallback seguro
panic("crypto/rand indisponível: " + err.Error())
}
id := hex.EncodeToString(buf[:])
exp := time.Now().Add(ss.ttl)
ss.mu.Lock()
ss.m[id] = exp
ss.mu.Unlock()
return id, exp
}
// valid confere se o id existe e não expirou; expirados são removidos no passe.
func (ss *sessionStore) valid(id string) bool {
if id == "" {
return false
}
now := time.Now()
ss.mu.Lock()
defer ss.mu.Unlock()
exp, ok := ss.m[id]
if !ok {
return false
}
if now.After(exp) {
delete(ss.m, id)
return false
}
// limpeza oportunista: mapa cresceu além do saudável → poda expirados
if len(ss.m) > 4096 {
for k, e := range ss.m {
if now.After(e) {
delete(ss.m, k)
}
}
}
return true
}
// revoke remove a sessão; devolve true se existia.
func (ss *sessionStore) revoke(id string) bool {
ss.mu.Lock()
defer ss.mu.Unlock()
_, ok := ss.m[id]
delete(ss.m, id)
return ok
}