security: address medium risks in jwt, redis ordering and body limit
This commit is contained in:
@@ -188,6 +188,15 @@ func (h *Handler) jwtMiddleware(next http.Handler) http.Handler {
|
|||||||
return
|
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)
|
// Проверяем что тенант существует (был создан при /ui/api/auth)
|
||||||
jwtTenant, ok := h.store.GetBySub(claims.Sub)
|
jwtTenant, ok := h.store.GetBySub(claims.Sub)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
@@ -24,6 +25,12 @@ import (
|
|||||||
// nil означает режим "только память" — все функции тихо no-op.
|
// nil означает режим "только память" — все функции тихо no-op.
|
||||||
var Client *redis.Client
|
var Client *redis.Client
|
||||||
|
|
||||||
|
var (
|
||||||
|
seqMu sync.Mutex
|
||||||
|
queueWriteSeq = map[string]uint64{}
|
||||||
|
tenantWriteSeq = map[string]uint64{}
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// redisHashTenants — HASH: tenantID → JSON тенанта
|
// redisHashTenants — HASH: tenantID → JSON тенанта
|
||||||
redisHashTenants = "ssq:tenants"
|
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))
|
log.Warnf("persistence: queue %q too large for Redis (%d bytes), skipping", key, len(data))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
writeSeq := nextQueueWriteSeq(key)
|
||||||
asyncWrite(func() {
|
asyncWrite(func() {
|
||||||
|
if !isLatestQueueWriteSeq(key, writeSeq) {
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := Client.HSet(ctx, redisHashQueues, key, string(data)).Err(); err != nil {
|
if err := Client.HSet(ctx, redisHashQueues, key, string(data)).Err(); err != nil {
|
||||||
@@ -99,7 +110,11 @@ func DeleteQueue(key string) {
|
|||||||
if Client == nil {
|
if Client == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
writeSeq := nextQueueWriteSeq(key)
|
||||||
asyncWrite(func() {
|
asyncWrite(func() {
|
||||||
|
if !isLatestQueueWriteSeq(key, writeSeq) {
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := Client.HDel(ctx, redisHashQueues, key).Err(); err != nil {
|
if err := Client.HDel(ctx, redisHashQueues, key).Err(); err != nil {
|
||||||
@@ -153,7 +168,11 @@ func SaveTenantRaw(id string, jsonData []byte) {
|
|||||||
// Копируем bytes — вызывающий может переиспользовать буфер
|
// Копируем bytes — вызывающий может переиспользовать буфер
|
||||||
dataCopy := make([]byte, len(jsonData))
|
dataCopy := make([]byte, len(jsonData))
|
||||||
copy(dataCopy, jsonData)
|
copy(dataCopy, jsonData)
|
||||||
|
writeSeq := nextTenantWriteSeq(id)
|
||||||
asyncWrite(func() {
|
asyncWrite(func() {
|
||||||
|
if !isLatestTenantWriteSeq(id, writeSeq) {
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := Client.HSet(ctx, redisHashTenants, id, string(dataCopy)).Err(); err != nil {
|
if err := Client.HSet(ctx, redisHashTenants, id, string(dataCopy)).Err(); err != nil {
|
||||||
@@ -167,7 +186,11 @@ func DeleteTenant(id string) {
|
|||||||
if Client == nil {
|
if Client == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
writeSeq := nextTenantWriteSeq(id)
|
||||||
asyncWrite(func() {
|
asyncWrite(func() {
|
||||||
|
if !isLatestTenantWriteSeq(id, writeSeq) {
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := Client.HDel(ctx, redisHashTenants, id).Err(); err != nil {
|
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 при старте.
|
// LoadAllTenantsRaw — загружает всех тенантов из Redis при старте.
|
||||||
// Возвращает map[tenantID]rawJSON — десериализацию делает tenant_store.go.
|
// Возвращает map[tenantID]rawJSON — десериализацию делает tenant_store.go.
|
||||||
func LoadAllTenantsRaw() (map[string][]byte, error) {
|
func LoadAllTenantsRaw() (map[string][]byte, error) {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import (
|
|||||||
func New(tenantStore *tenant.TenantStore, adminToken string) http.Handler {
|
func New(tenantStore *tenant.TenantStore, adminToken string) http.Handler {
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
|
|
||||||
// Fix #3.5: глобальный лимит размера тела HTTP запроса — 1MB
|
// Fix #3.5: глобальный лимит размера тела HTTP запроса — 3MB
|
||||||
// Защита от OOM при отправке гигантских batch/message requests
|
// Защита от OOM при отправке гигантских batch/message requests
|
||||||
r.Use(requestBodyLimitMiddleware)
|
r.Use(requestBodyLimitMiddleware)
|
||||||
|
|
||||||
@@ -57,12 +57,12 @@ func New(tenantStore *tenant.TenantStore, adminToken string) http.Handler {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// requestBodyLimitMiddleware — ограничивает размер тела HTTP запроса до 1MB.
|
// requestBodyLimitMiddleware — ограничивает размер тела HTTP запроса до 3MB.
|
||||||
// Защита от OOM при отправке гигантских messages/batch (Fix #3.5).
|
// Защита от OOM при отправке гигантских messages/batch (Fix #3.5).
|
||||||
func requestBodyLimitMiddleware(next http.Handler) http.Handler {
|
func requestBodyLimitMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Body != nil {
|
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)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user