diff --git a/internal/web/service/panel/panel.go b/internal/web/service/panel/panel.go index 62ce7d10a..c4db7017c 100644 --- a/internal/web/service/panel/panel.go +++ b/internal/web/service/panel/panel.go @@ -43,6 +43,7 @@ type PanelUpdateInfo struct { const ( panelUpdaterURL = "https://raw.githubusercontent.com/MHSanaei/3x-ui/main/update.sh" maxPanelUpdaterBytes = 2 << 20 + maxPanelReleaseBytes = 2 << 20 // devReleaseTag is the fixed-tag rolling pre-release the CI force-moves to the // newest main commit; the dev update channel installs from it. devReleaseTag = "dev-latest" @@ -450,7 +451,7 @@ func fetchPanelRelease(tag string) (*service.Release, error) { } var release service.Release - if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + if err := json.NewDecoder(io.LimitReader(resp.Body, maxPanelReleaseBytes)).Decode(&release); err != nil { return nil, err } return &release, nil diff --git a/internal/web/service/server.go b/internal/web/service/server.go index 57287aed6..8fa7db3ca 100644 --- a/internal/web/service/server.go +++ b/internal/web/service/server.go @@ -19,6 +19,7 @@ import ( "mime/multipart" stdnet "net" "net/http" + "net/netip" "net/url" "os" "os/exec" @@ -402,6 +403,10 @@ type MLKEM768Response struct { Client string `json:"client" example:"mlkem768-client"` } +// maxPublicIPBytes caps an IP echo service's answer; a textual IPv6 address +// with its line ending fits well within it. +const maxPublicIPBytes = 64 + func getPublicIP(url string) string { client := &http.Client{ Timeout: 3 * time.Second, @@ -425,17 +430,19 @@ func getPublicIP(url string) string { return "N/A" } - ip, err := io.ReadAll(resp.Body) + body, err := io.ReadAll(io.LimitReader(resp.Body, maxPublicIPBytes)) if err != nil { return "N/A" } - ipString := strings.TrimSpace(string(ip)) - if ipString == "" { + // The answer is cached and sent with every status sample, so only a bare + // address is accepted; an over-long one is cut above and fails to parse. + ip, err := netip.ParseAddr(strings.TrimSpace(string(body))) + if err != nil { return "N/A" } - return ipString + return ip.WithZone("").String() } var publicIPv4Services = []string{ @@ -921,6 +928,9 @@ const ( // maxXrayDigestBytes caps the .dgst checksum sidecar read; it is a few // hundred bytes in practice. maxXrayDigestBytes = 64 << 10 + // maxXrayReleasesBytes caps the GitHub release list read; a page of + // releases with all their assets is a few MiB. + maxXrayReleasesBytes = 32 << 20 ) func (s *ServerService) GetXrayVersions() ([]string, error) { @@ -941,7 +951,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) { // Check HTTP status code - GitHub API returns object instead of array on error if resp.StatusCode != http.StatusOK { - bodyBytes, _ := io.ReadAll(resp.Body) + bodyBytes, _ := io.ReadAll(io.LimitReader(resp.Body, maxXrayReleasesBytes)) var errorResponse struct { Message string `json:"message"` } @@ -953,7 +963,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) { buffer := bytes.NewBuffer(make([]byte, bufferSize)) buffer.Reset() - if _, err := buffer.ReadFrom(resp.Body); err != nil { + if _, err := buffer.ReadFrom(io.LimitReader(resp.Body, maxXrayReleasesBytes)); err != nil { return nil, err } diff --git a/internal/web/service/server_public_ip_async_test.go b/internal/web/service/server_public_ip_async_test.go index f01e2f1e0..1f56717d7 100644 --- a/internal/web/service/server_public_ip_async_test.go +++ b/internal/web/service/server_public_ip_async_test.go @@ -564,3 +564,30 @@ func TestPublicIPv6BackoffPreservesIPv4RefreshAndRetry(t *testing.T) { } }) } + +// The IP echo services are third parties; their answer is cached for the life +// of the process and copied into every status sample. +func TestGetPublicIPAcceptsOnlyAnAddress(t *testing.T) { + tests := []struct { + name string + body string + want string + }{ + {name: "ipv4 with line ending", body: "203.0.113.7\n", want: "203.0.113.7"}, + {name: "ipv6", body: "2001:db8::7", want: "2001:db8::7"}, + {name: "not an address", body: "blocked", want: "N/A"}, + {name: "address followed by megabytes", body: "203.0.113.7\n" + strings.Repeat("1", 8<<20), want: "N/A"}, + {name: "empty", body: "", want: "N/A"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(tt.body)) + })) + defer server.Close() + if got := getPublicIP(server.URL); got != tt.want { + t.Fatalf("getPublicIP = %.40q (%d bytes), want %q", got, len(got), tt.want) + } + }) + } +}