security: address medium risks in jwt, redis ordering and body limit

This commit is contained in:
Naeel
2026-04-10 19:44:34 +03:00
parent a5e9bfb15c
commit 3b4fe0bb3a
3 changed files with 61 additions and 3 deletions
+9
View File
@@ -188,6 +188,15 @@ func (h *Handler) jwtMiddleware(next http.Handler) http.Handler {
return
}
// Повторно валидируем токен в nubes API на каждый UI API запрос,
// чтобы отозванные токены не оставались валидными до повторного логина.
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
if err := auth.PingNubesAPI(ctx, h.nubesEndpoint, token); err != nil {
jsonErr(w, http.StatusForbidden, "token rejected by cloud API: "+err.Error())
return
}
// Проверяем что тенант существует (был создан при /ui/api/auth)
jwtTenant, ok := h.store.GetBySub(claims.Sub)
if !ok {
+49
View File
@@ -12,6 +12,7 @@ import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/redis/go-redis/v9"
@@ -24,6 +25,12 @@ import (
// nil означает режим "только память" — все функции тихо no-op.
var Client *redis.Client
var (
seqMu sync.Mutex
queueWriteSeq = map[string]uint64{}
tenantWriteSeq = map[string]uint64{}
)
const (
// redisHashTenants — HASH: tenantID → JSON тенанта
redisHashTenants = "ssq:tenants"
@@ -85,7 +92,11 @@ func SaveQueue(key string, queue *models.Queue) {
log.Warnf("persistence: queue %q too large for Redis (%d bytes), skipping", key, len(data))
return
}
writeSeq := nextQueueWriteSeq(key)
asyncWrite(func() {
if !isLatestQueueWriteSeq(key, writeSeq) {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := Client.HSet(ctx, redisHashQueues, key, string(data)).Err(); err != nil {
@@ -99,7 +110,11 @@ func DeleteQueue(key string) {
if Client == nil {
return
}
writeSeq := nextQueueWriteSeq(key)
asyncWrite(func() {
if !isLatestQueueWriteSeq(key, writeSeq) {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := Client.HDel(ctx, redisHashQueues, key).Err(); err != nil {
@@ -153,7 +168,11 @@ func SaveTenantRaw(id string, jsonData []byte) {
// Копируем bytes — вызывающий может переиспользовать буфер
dataCopy := make([]byte, len(jsonData))
copy(dataCopy, jsonData)
writeSeq := nextTenantWriteSeq(id)
asyncWrite(func() {
if !isLatestTenantWriteSeq(id, writeSeq) {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := Client.HSet(ctx, redisHashTenants, id, string(dataCopy)).Err(); err != nil {
@@ -167,7 +186,11 @@ func DeleteTenant(id string) {
if Client == nil {
return
}
writeSeq := nextTenantWriteSeq(id)
asyncWrite(func() {
if !isLatestTenantWriteSeq(id, writeSeq) {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := Client.HDel(ctx, redisHashTenants, id).Err(); err != nil {
@@ -176,6 +199,32 @@ func DeleteTenant(id string) {
})
}
func nextQueueWriteSeq(key string) uint64 {
seqMu.Lock()
defer seqMu.Unlock()
queueWriteSeq[key]++
return queueWriteSeq[key]
}
func isLatestQueueWriteSeq(key string, seq uint64) bool {
seqMu.Lock()
defer seqMu.Unlock()
return queueWriteSeq[key] == seq
}
func nextTenantWriteSeq(id string) uint64 {
seqMu.Lock()
defer seqMu.Unlock()
tenantWriteSeq[id]++
return tenantWriteSeq[id]
}
func isLatestTenantWriteSeq(id string, seq uint64) bool {
seqMu.Lock()
defer seqMu.Unlock()
return tenantWriteSeq[id] == seq
}
// LoadAllTenantsRaw — загружает всех тенантов из Redis при старте.
// Возвращает map[tenantID]rawJSON — десериализацию делает tenant_store.go.
func LoadAllTenantsRaw() (map[string][]byte, error) {
+3 -3
View File
@@ -26,7 +26,7 @@ import (
func New(tenantStore *tenant.TenantStore, adminToken string) http.Handler {
r := mux.NewRouter()
// Fix #3.5: глобальный лимит размера тела HTTP запроса — 1MB
// Fix #3.5: глобальный лимит размера тела HTTP запроса — 3MB
// Защита от OOM при отправке гигантских batch/message requests
r.Use(requestBodyLimitMiddleware)
@@ -57,12 +57,12 @@ func New(tenantStore *tenant.TenantStore, adminToken string) http.Handler {
return r
}
// requestBodyLimitMiddleware — ограничивает размер тела HTTP запроса до 1MB.
// requestBodyLimitMiddleware — ограничивает размер тела HTTP запроса до 3MB.
// Защита от OOM при отправке гигантских messages/batch (Fix #3.5).
func requestBodyLimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Body != nil {
r.Body = http.MaxBytesReader(w, r.Body, 1*1024*1024) // 1MB
r.Body = http.MaxBytesReader(w, r.Body, 3*1024*1024) // 3MB
}
next.ServeHTTP(w, r)
})