Browse Source

fix(subscription): bound outbound response body (#5493)

n0ctal 1 ngày trước cách đây
mục cha
commit
ecb0b0a9fa

+ 20 - 1
internal/web/service/outbound_subscription.go

@@ -3,6 +3,7 @@ package service
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
@@ -18,6 +19,24 @@ import (
 	"github.com/mhsanaei/3x-ui/v3/internal/util/link"
 )
 
+// maxOutboundSubscriptionBytes caps a single outbound subscription response.
+// It is larger than the 2 MiB user-facing subscription cap because an outbound
+// subscription may aggregate many upstream outbounds into one document.
+const maxOutboundSubscriptionBytes int64 = 8 << 20
+
+var errOutboundSubscriptionBodyTooLarge = errors.New("outbound subscription response body exceeds size limit")
+
+func readBoundedOutboundSubscriptionBody(r io.Reader) ([]byte, error) {
+	body, err := io.ReadAll(io.LimitReader(r, maxOutboundSubscriptionBytes+1))
+	if err != nil {
+		return nil, err
+	}
+	if int64(len(body)) > maxOutboundSubscriptionBytes {
+		return nil, fmt.Errorf("%w (limit: %d bytes)", errOutboundSubscriptionBodyTooLarge, maxOutboundSubscriptionBytes)
+	}
+	return body, nil
+}
+
 // OutboundSubscriptionService manages remote outbound subscriptions.
 type OutboundSubscriptionService struct {
 	settingService SettingService
@@ -281,7 +300,7 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript
 		s.recordError(sub, err)
 		return nil, err
 	}
-	body, err := io.ReadAll(resp.Body)
+	body, err := readBoundedOutboundSubscriptionBody(resp.Body)
 	if err != nil {
 		s.recordError(sub, err)
 		return nil, err

+ 26 - 0
internal/web/service/outbound_subscription_test.go

@@ -1,12 +1,38 @@
 package service
 
 import (
+	"bytes"
+	"errors"
 	"testing"
 
 	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 	"github.com/mhsanaei/3x-ui/v3/internal/util/link"
 )
 
+func TestReadBoundedOutboundSubscriptionBody(t *testing.T) {
+	t.Run("accepts body at the limit", func(t *testing.T) {
+		want := bytes.Repeat([]byte("a"), int(maxOutboundSubscriptionBytes))
+		got, err := readBoundedOutboundSubscriptionBody(bytes.NewReader(want))
+		if err != nil {
+			t.Fatalf("readBoundedOutboundSubscriptionBody: %v", err)
+		}
+		if !bytes.Equal(got, want) {
+			t.Fatalf("body mismatch: got %d bytes, want %d", len(got), len(want))
+		}
+	})
+
+	t.Run("rejects body over the limit", func(t *testing.T) {
+		body := bytes.Repeat([]byte("b"), int(maxOutboundSubscriptionBytes)+1)
+		got, err := readBoundedOutboundSubscriptionBody(bytes.NewReader(body))
+		if !errors.Is(err, errOutboundSubscriptionBodyTooLarge) {
+			t.Fatalf("error = %v, want errOutboundSubscriptionBodyTooLarge", err)
+		}
+		if got != nil {
+			t.Fatalf("oversized body returned %d bytes, want nil", len(got))
+		}
+	})
+}
+
 func TestDefaultPrefixNumber(t *testing.T) {
 	mk := func(id int, prefix string) *model.OutboundSubscription {
 		return &model.OutboundSubscription{Id: id, TagPrefix: prefix}