From 61b59bb868702dd4192a081755c1e9b131360e51 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 15 Jul 2026 04:36:30 +0200 Subject: [PATCH] fix(integration): cap WARP API response body size doWarpRequest read the response with an unbounded io.ReadAll, unlike the sibling NordVPN client which already caps every read at maxResponseSize. A hostile panel egress proxy or a MITM on the Cloudflare WARP endpoint could stream an arbitrarily large body and force the panel into an unbounded allocation. Wrap the body in an io.LimitReader(maxResponseSize) to match the NordVPN client. --- internal/web/service/integration/warp.go | 2 +- .../service/integration/warp_response_test.go | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 internal/web/service/integration/warp_response_test.go diff --git a/internal/web/service/integration/warp.go b/internal/web/service/integration/warp.go index 3f6c98997..14598b42e 100644 --- a/internal/web/service/integration/warp.go +++ b/internal/web/service/integration/warp.go @@ -238,7 +238,7 @@ func (s *WarpService) doWarpRequest(req *http.Request) ([]byte, error) { } defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) + body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize)) if err != nil { return nil, err } diff --git a/internal/web/service/integration/warp_response_test.go b/internal/web/service/integration/warp_response_test.go new file mode 100644 index 000000000..d37ca1f9b --- /dev/null +++ b/internal/web/service/integration/warp_response_test.go @@ -0,0 +1,42 @@ +package integration + +import ( + "bytes" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database" +) + +// A hostile egress proxy (or a MITM on the WARP endpoint) could stream an +// arbitrarily large body; doWarpRequest must cap the read at maxResponseSize so +// the panel cannot be forced into an unbounded allocation. +func TestDoWarpRequestCapsResponseBody(t *testing.T) { + if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { + t.Fatalf("InitDB: %v", err) + } + t.Cleanup(func() { _ = database.CloseDB() }) + + oversize := maxResponseSize + 4096 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(bytes.Repeat([]byte("a"), oversize)) + })) + defer srv.Close() + + s := &WarpService{} + req, err := http.NewRequest(http.MethodGet, srv.URL, nil) + if err != nil { + t.Fatalf("NewRequest: %v", err) + } + + body, err := s.doWarpRequest(req) + if err != nil { + t.Fatalf("doWarpRequest: %v", err) + } + if len(body) != maxResponseSize { + t.Fatalf("response body not capped: got %d bytes, want %d", len(body), maxResponseSize) + } +}