diff --git a/internal/web/service/node.go b/internal/web/service/node.go index 95f02d992..4c5184594 100644 --- a/internal/web/service/node.go +++ b/internal/web/service/node.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net" "net/http" "net/url" @@ -1203,6 +1204,10 @@ func (s *NodeService) withOutboundBridge(nodeID int, outboundTag string, fn func fn(proxyURL) } +// A status envelope holds a handful of scalars; the cap keeps a hostile or +// broken node from dictating the master's allocation on every heartbeat. +const maxProbeBodyBytes = 1 << 20 // 1 MiB + func (s *NodeService) probe(ctx context.Context, n *model.Node, proxyURL string) (HeartbeatPatch, error) { patch := HeartbeatPatch{LastHeartbeat: time.Now().Unix()} @@ -1285,7 +1290,7 @@ func (s *NodeService) probe(ctx context.Context, n *model.Node, proxyURL string) } `json:"netIO"` } `json:"obj"` } - if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil { + if err := json.NewDecoder(io.LimitReader(resp.Body, maxProbeBodyBytes)).Decode(&envelope); err != nil { patch.LastError = "decode response: " + err.Error() return patch, err } diff --git a/internal/web/service/node_probe_body_cap_test.go b/internal/web/service/node_probe_body_cap_test.go new file mode 100644 index 000000000..dda9f00a8 --- /dev/null +++ b/internal/web/service/node_probe_body_cap_test.go @@ -0,0 +1,47 @@ +package service + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "strings" + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database/model" +) + +// A node answers the probe over a connection the master does not control in the +// skip/pin TLS modes, so an oversized status body must be rejected rather than +// buffered whole by encoding/json. +func TestProbeRejectsOversizedStatusBody(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"success":true,"obj":{"cpuPct":1,"panelVersion":"`)) + pad := strings.Repeat("x", 1<<20) + for i := 0; i < 3; i++ { + _, _ = w.Write([]byte(pad)) + } + _, _ = w.Write([]byte(`"}}`)) + })) + defer srv.Close() + + u, err := url.Parse(srv.URL) + if err != nil { + t.Fatalf("parse url: %v", err) + } + port, err := strconv.Atoi(u.Port()) + if err != nil { + t.Fatalf("parse port: %v", err) + } + n := &model.Node{ + Id: 1, Name: "big", Scheme: "http", Address: u.Hostname(), Port: port, + BasePath: "/", Enable: true, AllowPrivateAddress: true, TlsVerifyMode: "skip", + } + + svc := &NodeService{} + if _, err := svc.Probe(context.Background(), n); err == nil { + t.Fatal("Probe accepted a 3 MiB status body, want an error") + } +}