mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 04:00:57 +00:00
fix(xray): verify the release archive checksum before installing (#5396)
* fix(xray): verify the release archive checksum before installing
UpdateXray downloaded the Xray-core release zip and installed the binary
from it after only a TLS fetch, an HTTP-200 check and a size cap — the
archive itself was never verified, so a corrupted or tampered release
asset would be extracted and run as the panel's xray binary.
Verify the downloaded archive against the SHA2-256 published in the
release's .dgst sidecar (which XTLS ships next to every asset) before
installing, and abort the update on mismatch, a missing/short SHA2-256
entry, or an unreachable .dgst. The digest parser and fetch are covered by
tests, including the real .dgst line format ("SHA2-256= <hex>").
* address review: clearer warning + re-download guidance on checksum mismatch
Per review feedback on the PR: on a SHA-256 mismatch, surface a plain-language
warning that the downloaded archive is corrupted or differs from the official
release and that the user should exit and re-download, instead of a terse
"checksum mismatch" error. The install still aborts so a mismatched binary is
never run; the message now tells the user the safe next step.
This commit is contained in:
parent
abffa8f6c9
commit
2bb851dd50
2 changed files with 134 additions and 0 deletions
|
|
@ -4,6 +4,8 @@ import (
|
|||
"archive/zip"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -685,6 +687,9 @@ func (s *ServerService) sampleCPUUtilization() (float64, error) {
|
|||
const (
|
||||
maxXrayArchiveBytes = 200 << 20
|
||||
maxXrayBinaryBytes = 200 << 20
|
||||
// maxXrayDigestBytes caps the .dgst checksum sidecar read; it is a few
|
||||
// hundred bytes in practice.
|
||||
maxXrayDigestBytes = 64 << 10
|
||||
)
|
||||
|
||||
func (s *ServerService) GetXrayVersions() ([]string, error) {
|
||||
|
|
@ -826,10 +831,67 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
|
|||
return "", fmt.Errorf("download xray: archive exceeds %d bytes", maxXrayArchiveBytes)
|
||||
}
|
||||
|
||||
// Verify the archive against the SHA2-256 published in the release's .dgst
|
||||
// sidecar before installing it. TLS protects the transport, not the artifact;
|
||||
// a corrupted or tampered asset must not be installed and run as xray.
|
||||
want, err := s.fetchXrayDigestSHA256(client, url+".dgst")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||
return "", err
|
||||
}
|
||||
hasher := sha256.New()
|
||||
if _, err := io.Copy(hasher, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if got := hex.EncodeToString(hasher.Sum(nil)); !strings.EqualFold(got, want) {
|
||||
// User-facing warning: the archive's SHA-256 does not match the official
|
||||
// release checksum, so the download is corrupted or has been tampered
|
||||
// with. Abort the install so a bad binary is never run, and tell the user
|
||||
// to retry/re-download rather than proceed with a mismatched image.
|
||||
return "", fmt.Errorf("Xray update aborted: the downloaded archive does not match the official SHA-256 checksum, so the image is corrupted or differs from the official release. Please exit and re-download the official image, then try again (expected %s, got %s)", want, got)
|
||||
}
|
||||
|
||||
ok = true
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// fetchXrayDigestSHA256 downloads the .dgst sidecar XTLS publishes next to each
|
||||
// release asset and returns the SHA2-256 hex digest it lists.
|
||||
func (s *ServerService) fetchXrayDigestSHA256(client *http.Client, dgstURL string) (string, error) {
|
||||
resp, err := client.Get(dgstURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("download xray checksum: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("download xray checksum: unexpected HTTP %d", resp.StatusCode)
|
||||
}
|
||||
raw, err := io.ReadAll(io.LimitReader(resp.Body, maxXrayDigestBytes))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("download xray checksum: %w", err)
|
||||
}
|
||||
return parseXrayDigestSHA256(raw)
|
||||
}
|
||||
|
||||
// parseXrayDigestSHA256 extracts the lowercase SHA2-256 hex from an XTLS .dgst
|
||||
// file, whose lines are "ALGO= <hex>" (the relevant one being "SHA2-256= ...").
|
||||
func parseXrayDigestSHA256(dgst []byte) (string, error) {
|
||||
for _, line := range strings.Split(string(dgst), "\n") {
|
||||
rest, ok := strings.CutPrefix(strings.TrimSpace(line), "SHA2-256=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
h := strings.ToLower(strings.TrimSpace(rest))
|
||||
if len(h) != 64 {
|
||||
return "", fmt.Errorf("xray checksum: malformed SHA2-256 entry in digest")
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
return "", fmt.Errorf("xray checksum: no SHA2-256 entry in digest")
|
||||
}
|
||||
|
||||
func (s *ServerService) UpdateXray(version string) error {
|
||||
versions, err := s.GetXrayVersions()
|
||||
if err != nil {
|
||||
|
|
|
|||
72
internal/web/service/server_xray_checksum_test.go
Normal file
72
internal/web/service/server_xray_checksum_test.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A real XTLS .dgst sidecar (Xray-linux-64.zip.dgst, v26.3.27): lines are
|
||||
// "ALGO= <hex>", and the algorithm label is "SHA2-256", not "SHA256".
|
||||
const sampleXrayDgst = `# Hash Values
|
||||
|
||||
MD5= ee4e2ff74948a9b464624b1cabc44409
|
||||
SHA1= b55b06e74e89083b9cedfdecf0d68b579cd2af72
|
||||
SHA2-256= 23cd9af937744d97776ee35ecad4972cf4b2109d1e0fe6be9930467608f7c8ae
|
||||
SHA2-512= e8bc40a0687cac184bbe4b5c1f047e69064ccedc489fb25e208889ae287bbf8736dff16b108d68fc00dc33edc8bb53502e47a9698a277f4f51b67b83d899e518
|
||||
`
|
||||
|
||||
const wantSHA = "23cd9af937744d97776ee35ecad4972cf4b2109d1e0fe6be9930467608f7c8ae"
|
||||
|
||||
func TestParseXrayDigestSHA256(t *testing.T) {
|
||||
got, err := parseXrayDigestSHA256([]byte(sampleXrayDgst))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if got != wantSHA {
|
||||
t.Fatalf("sha = %q, want %q", got, wantSHA)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseXrayDigestSHA256_Errors(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in string
|
||||
}{
|
||||
{"no-sha256-line", "MD5= abc\nSHA1= def\n"},
|
||||
{"malformed-short", "SHA2-256= deadbeef\n"},
|
||||
{"empty", ""},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if _, err := parseXrayDigestSHA256([]byte(tc.in)); err == nil {
|
||||
t.Fatalf("%s: expected an error", tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchXrayDigestSHA256(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(sampleXrayDgst))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
got, err := (&ServerService{}).fetchXrayDigestSHA256(srv.Client(), srv.URL+"/Xray-linux-64.zip.dgst")
|
||||
if err != nil {
|
||||
t.Fatalf("fetch: %v", err)
|
||||
}
|
||||
if got != wantSHA {
|
||||
t.Fatalf("sha = %q, want %q", got, wantSHA)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchXrayDigestSHA256_HTTPError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "nope", http.StatusNotFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if _, err := (&ServerService{}).fetchXrayDigestSHA256(srv.Client(), srv.URL+"/missing.dgst"); err == nil {
|
||||
t.Fatal("expected an error on HTTP 404")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue