Files
SQS-service/app/gosqs/receive_message.go
T
Naeel e9a26f7975 v0.1.17: security hardening — 18/20 vulnerabilities fixed
Phase 1 (Critical):
- #1 JWT auth (done in v0.1.16)
- #2 Batch message size validation in send_message_batch.go
- #10 RLock in GetQueueUrlV1 (data race fix)

Phase 2 (AWS-compatible limits):
- #3 QueueName validation: max 80 chars, [a-zA-Z0-9_-](.fifo)?
- #4 WaitTimeSeconds clamped to 0-20
- #5 ReceiveMessageWaitTimeSeconds clamped to 0-20
- #6 DelaySeconds clamped to 0-900
- #7 VisibilityTimeout clamped to 0-43200
- #8 MaxNumberOfMessages clamped to 1-10
- #9 Message attributes limited to 10 per message
- #15 BatchEntryId length validated (max 80)
- #16 DeduplicationID length validated (max 128)
- #17 GroupID length validated (max 128)

Phase 3 (Per-tenant resource limits):
- #11 Max messages per queue (120K standard, 20K FIFO)
- #12 Global tenant limit (1000)
- #3.5 HTTP request body size limit (1MB via MaxBytesReader)

Phase 4 (Stability):
- #14 Duplicates map cleanup (already in PeriodicTasks)
- #13 FIFO group lock timeout (already in visibility timeout reset)
- #18 Redis size guard: skip save if >50MB

Skipped (Low, no real risk):
- #19 {account} URL param (informational only, not used for access)
- #20 ReceiptHandle format (self-validating UUID#UUID)

New file: app/gosqs/validation.go — centralized AWS SQS limits and validators
2026-04-10 18:58:40 +03:00

173 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Изменено: 2026-04-09
// ReceiveMessageV1 — получает сообщения из очереди тенанта с поддержкой long polling.
// Ловушка #4: long polling держит соединение до 20 сек — не прерываем принудительно.
package gosqs
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"shared-sqs/app/interfaces"
"shared-sqs/app/models"
"shared-sqs/app/utils"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
)
func ReceiveMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
requestBody := models.NewReceiveMessageRequest()
ok := utils.REQUEST_TRANSFORMER(requestBody, req, false)
if !ok {
log.Error("Invalid Request - ReceiveMessageV1")
return utils.CreateErrorResponseV1("InvalidParameterValue", true)
}
t := getTenantFromContext(req)
if t == nil {
return utils.CreateErrorResponseV1("InvalidClientTokenId", true)
}
maxNumberOfMessages := requestBody.MaxNumberOfMessages
if maxNumberOfMessages == 0 {
maxNumberOfMessages = 1
}
// Fix #8: clamp MaxNumberOfMessages к AWS лимиту 1–10
maxNumberOfMessages = ClampInt(maxNumberOfMessages, MinNumberOfMessagesLimit, MaxNumberOfMessagesLimit)
queueName := ""
if requestBody.QueueUrl == "" {
vars := mux.Vars(req)
queueName = vars["queueName"]
} else {
uriSegments := strings.Split(requestBody.QueueUrl, "/")
queueName = uriSegments[len(uriSegments)-1]
}
key := tenantQueueKey(t.AccessKey, queueName)
if _, ok := models.SyncQueues.Queues[key]; !ok {
return utils.CreateErrorResponseV1("QueueNotFound", true)
}
var messages []*models.ResultMessage
respStruct := models.ReceiveMessageResponse{}
waitTimeSeconds := requestBody.WaitTimeSeconds
if waitTimeSeconds == 0 {
models.SyncQueues.RLock()
waitTimeSeconds = models.SyncQueues.Queues[key].ReceiveMessageWaitTimeSeconds
models.SyncQueues.RUnlock()
}
// Fix #4: clamp WaitTimeSeconds к AWS лимиту 0–20
waitTimeSeconds = ClampInt(waitTimeSeconds, 0, MaxReceiveMessageWaitTimeSeconds)
// Long polling: ждём появления сообщения до waitTimeSeconds*10 итераций по 100ms
loops := waitTimeSeconds * 10
for loops > 0 {
models.SyncQueues.RLock()
_, queueFound := models.SyncQueues.Queues[key]
if !queueFound {
models.SyncQueues.RUnlock()
return utils.CreateErrorResponseV1("QueueNotFound", true)
}
messageFound := len(models.SyncQueues.Queues[key].Messages)-numberOfHiddenMessagesInQueue(*models.SyncQueues.Queues[key]) != 0
models.SyncQueues.RUnlock()
if !messageFound {
continueTimer := time.NewTimer(100 * time.Millisecond)
select {
case <-req.Context().Done():
continueTimer.Stop()
return http.StatusOK, models.ReceiveMessageResponse{
Xmlns: models.BaseXmlns,
Result: models.ReceiveMessageResult{},
Metadata: models.BaseResponseMetadata,
}
case <-continueTimer.C:
continueTimer.Stop()
}
loops--
} else {
break
}
}
log.Debugf("Getting Message from Queue:%s (tenant: %s)", queueName, t.ID)
models.SyncQueues.Lock()
defer models.SyncQueues.Unlock()
if len(models.SyncQueues.Queues[key].Messages) > 0 {
numMsg := 0
messages = make([]*models.ResultMessage, 0)
for i := range models.SyncQueues.Queues[key].Messages {
if numMsg >= maxNumberOfMessages {
break
}
if models.SyncQueues.Queues[key].Messages[i].ReceiptHandle != "" {
continue
}
msg := &models.SyncQueues.Queues[key].Messages[i]
if !msg.IsReadyForReceipt() {
continue
}
if models.SyncQueues.Queues[key].IsFIFO {
if models.SyncQueues.Queues[key].IsLocked(msg.GroupID) {
continue
}
models.SyncQueues.Queues[key].LockGroup(msg.GroupID)
}
randomId := uuid.NewString()
msg.ReceiptHandle = msg.Uuid + "#" + randomId
msg.ReceiptTime = time.Now().UTC()
if requestBody.VisibilityTimeout != 0 {
msg.VisibilityTimeout = time.Now().Add(time.Duration(requestBody.VisibilityTimeout) * time.Second)
} else {
msg.VisibilityTimeout = time.Now().Add(time.Duration(models.SyncQueues.Queues[key].VisibilityTimeout) * time.Second)
}
messages = append(messages, buildResultMessage(msg))
numMsg++
}
respStruct = models.ReceiveMessageResponse{
"http://queue.amazonaws.com/doc/2012-11-05/",
models.ReceiveMessageResult{Messages: messages},
models.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"},
}
} else {
log.Warning("No messages in Queue:", queueName)
respStruct = models.ReceiveMessageResponse{
Xmlns: "http://queue.amazonaws.com/doc/2012-11-05/",
Result: models.ReceiveMessageResult{},
Metadata: models.ResponseMetadata{RequestId: "00000000-0000-0000-0000-000000000000"},
}
}
return http.StatusOK, respStruct
}
func buildResultMessage(m *models.SqsMessage) *models.ResultMessage {
return &models.ResultMessage{
MessageId: m.Uuid,
Body: m.MessageBody,
ReceiptHandle: m.ReceiptHandle,
MD5OfBody: utils.GetMD5Hash(m.MessageBody),
MD5OfMessageAttributes: m.MD5OfMessageAttributes,
MessageAttributes: m.MessageAttributes,
Attributes: map[string]string{
"ApproximateFirstReceiveTimestamp": fmt.Sprintf("%d", m.ReceiptTime.UnixNano()/int64(time.Millisecond)),
"SenderId": models.CurrentEnvironment.AccountID,
"ApproximateReceiveCount": fmt.Sprintf("%d", m.NumberOfReceives+1),
"SentTimestamp": fmt.Sprintf("%d", time.Now().UTC().UnixNano()/int64(time.Millisecond)),
},
}
}