mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-01 18:56:05 +03:00
fix(server): bound the public-IP lookup and accept only an address
getPublicIP read a third-party IP echo service's body whole and cached whatever came back as the panel's address, so one misbehaving service could make the panel inflate an endless response at startup, and an error page became the address shown in every status sample. The read is capped at 64 bytes and only a parsable IP is accepted; anything else counts as unavailable. The GitHub release lookups are capped as well.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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: "<html>blocked</html>", 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user