From 0af2aa0266bdf48ac18290265db272f0858338a9 Mon Sep 17 00:00:00 2001 From: jyxjjj <773933146@qq.com> Date: Tue, 19 May 2026 10:49:35 +0800 Subject: [PATCH 1/5] fix(offline-download): block cloud metadata endpoints - Add shared validation for cloud metadata endpoint URLs - Reuse validation in offline download entry and HTTP backends - Cover direct, DNS, private URL, and redirect validation cases Co-authored-by: Codex <267193182+codex@users.noreply.github.com> Signed-off-by: jyxjjj <16695261+jyxjjj@users.noreply.github.com> --- internal/offline_download/http/client.go | 7 +- internal/offline_download/tool/add.go | 4 + .../offline_download/tool/metadata_url.go | 91 +++++++++++++++++++ .../tool/metadata_url_test.go | 74 +++++++++++++++ .../offline_download/transmission/client.go | 10 +- 5 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 internal/offline_download/tool/metadata_url.go create mode 100644 internal/offline_download/tool/metadata_url_test.go diff --git a/internal/offline_download/http/client.go b/internal/offline_download/http/client.go index 5dcb6d590..77093aa18 100644 --- a/internal/offline_download/http/client.go +++ b/internal/offline_download/http/client.go @@ -50,6 +50,10 @@ func (s SimpleHttp) Status(task *tool.DownloadTask) (*tool.Status, error) { } func (s SimpleHttp) Run(task *tool.DownloadTask) error { + if err := tool.ValidateOfflineDownloadURL(task.Ctx(), task.Url); err != nil { + return err + } + streamPut := task.DeletePolicy == tool.UploadDownloadStream method := http.MethodGet if streamPut { @@ -63,7 +67,8 @@ func (s SimpleHttp) Run(task *tool.DownloadTask) error { if streamPut { req.Header.Set("Range", "bytes=0-") } - resp, err := s.client.Do(req) + client := tool.NewOfflineDownloadHTTPClient(s.client) + resp, err := client.Do(req) if err != nil { return err } diff --git a/internal/offline_download/tool/add.go b/internal/offline_download/tool/add.go index 0f574571e..a30f33d47 100644 --- a/internal/offline_download/tool/add.go +++ b/internal/offline_download/tool/add.go @@ -44,6 +44,10 @@ type AddURLArgs struct { } func AddURL(ctx context.Context, args *AddURLArgs) (task.TaskExtensionInfo, error) { + if err := ValidateOfflineDownloadURL(ctx, args.URL); err != nil { + return nil, err + } + // check storage storage, dstDirActualPath, err := op.GetStorageAndActualPath(args.DstDirPath) if err != nil { diff --git a/internal/offline_download/tool/metadata_url.go b/internal/offline_download/tool/metadata_url.go new file mode 100644 index 000000000..5d76a6dbd --- /dev/null +++ b/internal/offline_download/tool/metadata_url.go @@ -0,0 +1,91 @@ +package tool + +import ( + "context" + "errors" + "net" + "net/http" + "net/url" + "strings" +) + +var ( + ErrCloudMetadataEndpoint = errors.New("access to cloud metadata endpoint is not allowed") + lookupIPAddr = net.DefaultResolver.LookupIPAddr +) + +func ValidateOfflineDownloadURL(ctx context.Context, rawURL string) error { + rawURL = strings.TrimSpace(rawURL) + if rawURL == "" { + return nil + } + if ctx == nil { + ctx = context.Background() + } + + u, err := url.Parse(rawURL) + if err == nil && u.Host != "" { + return validateOfflineDownloadHost(ctx, u.Hostname()) + } + + if err == nil && u.Scheme != "" { + return nil + } + + // Keep scheme-less URLs compatible: only reject direct metadata IP forms here, + // instead of treating any leading path segment as a hostname and doing DNS. + host := strings.Trim(rawURL, "[]") + host, _, _ = strings.Cut(host, "/") + host, _, _ = strings.Cut(host, "?") + host, _, _ = strings.Cut(host, "#") + if splitHost, _, err := net.SplitHostPort(host); err == nil { + host = splitHost + } + if ip := net.ParseIP(host); isCloudMetadataIP(ip) { + return ErrCloudMetadataEndpoint + } + return nil +} + +func NewOfflineDownloadHTTPClient(base http.Client) *http.Client { + client := base + previousCheckRedirect := client.CheckRedirect + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if err := ValidateOfflineDownloadURL(req.Context(), req.URL.String()); err != nil { + return err + } + if previousCheckRedirect != nil { + return previousCheckRedirect(req, via) + } + if len(via) >= 10 { + return errors.New("stopped after 10 redirects") + } + return nil + } + return &client +} + +func validateOfflineDownloadHost(ctx context.Context, host string) error { + if ip := net.ParseIP(host); ip != nil { + if isCloudMetadataIP(ip) { + return ErrCloudMetadataEndpoint + } + return nil + } + + addrs, err := lookupIPAddr(ctx, host) + if err != nil { + return err + } + for _, addr := range addrs { + if isCloudMetadataIP(addr.IP) { + return ErrCloudMetadataEndpoint + } + } + return nil +} + +func isCloudMetadataIP(ip net.IP) bool { + ip = ip.To4() + return ip != nil && ip[0] == 169 && ip[1] == 254 && ip[2] == 169 && ip[3] == 254 +} diff --git a/internal/offline_download/tool/metadata_url_test.go b/internal/offline_download/tool/metadata_url_test.go new file mode 100644 index 000000000..0bd56adc4 --- /dev/null +++ b/internal/offline_download/tool/metadata_url_test.go @@ -0,0 +1,74 @@ +package tool + +import ( + "context" + "errors" + "net" + "net/http" + "testing" +) + +func TestValidateOfflineDownloadURLRejectsCloudMetadataIP(t *testing.T) { + err := ValidateOfflineDownloadURL(context.Background(), "http://169.254.169.254/") + if !errors.Is(err, ErrCloudMetadataEndpoint) { + t.Fatalf("expected cloud metadata error, got %v", err) + } +} + +func TestValidateOfflineDownloadURLRejectsCloudMetadataIPWithoutScheme(t *testing.T) { + err := ValidateOfflineDownloadURL(context.Background(), "169.254.169.254") + if !errors.Is(err, ErrCloudMetadataEndpoint) { + t.Fatalf("expected cloud metadata error, got %v", err) + } +} + +func TestValidateOfflineDownloadURLRejectsCloudMetadataIPWithPort(t *testing.T) { + err := ValidateOfflineDownloadURL(context.Background(), "http://169.254.169.254:80/") + if !errors.Is(err, ErrCloudMetadataEndpoint) { + t.Fatalf("expected cloud metadata error, got %v", err) + } +} + +func TestValidateOfflineDownloadURLAllowsPublicURL(t *testing.T) { + err := ValidateOfflineDownloadURL(context.Background(), "http://8.8.8.8/") + if err != nil { + t.Fatalf("expected public URL to be allowed, got %v", err) + } +} + +func TestValidateOfflineDownloadURLAllowsPrivateURL(t *testing.T) { + err := ValidateOfflineDownloadURL(context.Background(), "http://192.168.1.10:8080/") + if err != nil { + t.Fatalf("expected private URL to be allowed, got %v", err) + } +} + +func TestValidateOfflineDownloadURLRejectsDomainResolvingToCloudMetadataIP(t *testing.T) { + previousLookup := lookupIPAddr + lookupIPAddr = func(ctx context.Context, host string) ([]net.IPAddr, error) { + if host != "metadata.example.test" { + t.Fatalf("unexpected host lookup: %s", host) + } + return []net.IPAddr{{IP: net.ParseIP("169.254.169.254")}}, nil + } + defer func() { + lookupIPAddr = previousLookup + }() + + err := ValidateOfflineDownloadURL(context.Background(), "http://metadata.example.test/") + if !errors.Is(err, ErrCloudMetadataEndpoint) { + t.Fatalf("expected cloud metadata error, got %v", err) + } +} + +func TestOfflineDownloadHTTPClientRejectsRedirectToCloudMetadataIP(t *testing.T) { + client := NewOfflineDownloadHTTPClient(http.Client{}) + req, err := http.NewRequest(http.MethodGet, "http://169.254.169.254/latest/meta-data/", nil) + if err != nil { + t.Fatalf("failed to build redirect request: %v", err) + } + err = client.CheckRedirect(req, nil) + if !errors.Is(err, ErrCloudMetadataEndpoint) { + t.Fatalf("expected cloud metadata error, got %v", err) + } +} diff --git a/internal/offline_download/transmission/client.go b/internal/offline_download/transmission/client.go index f86390fb8..100e7af5e 100644 --- a/internal/offline_download/transmission/client.go +++ b/internal/offline_download/transmission/client.go @@ -74,6 +74,10 @@ func (t *Transmission) IsReady() bool { } func (t *Transmission) AddURL(args *tool.AddUrlArgs) (string, error) { + if err := tool.ValidateOfflineDownloadURL(context.Background(), args.Url); err != nil { + return "", err + } + endpoint, err := url.Parse(args.Url) if err != nil { return "", errors.Wrap(err, "failed to parse transmission uri") @@ -84,7 +88,11 @@ func (t *Transmission) AddURL(args *tool.AddUrlArgs) (string, error) { } // http url for .torrent file if endpoint.Scheme == "http" || endpoint.Scheme == "https" { - resp, err := http.Get(args.Url) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, args.Url, nil) + if err != nil { + return "", err + } + resp, err := tool.NewOfflineDownloadHTTPClient(http.Client{}).Do(req) if err != nil { return "", errors.Wrap(err, "failed to get .torrent file") } From 560d6824ec699efa2bce0709ce2f7c3e1a81f0f7 Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Mon, 1 Jun 2026 02:13:59 +0800 Subject: [PATCH 2/5] Revert "fix(offline-download): block cloud metadata endpoints" This reverts commit 0af2aa0266bdf48ac18290265db272f0858338a9. --- internal/offline_download/http/client.go | 7 +- internal/offline_download/tool/add.go | 4 - .../offline_download/tool/metadata_url.go | 91 ------------------- .../tool/metadata_url_test.go | 74 --------------- .../offline_download/transmission/client.go | 10 +- 5 files changed, 2 insertions(+), 184 deletions(-) delete mode 100644 internal/offline_download/tool/metadata_url.go delete mode 100644 internal/offline_download/tool/metadata_url_test.go diff --git a/internal/offline_download/http/client.go b/internal/offline_download/http/client.go index 07623b931..86314031d 100644 --- a/internal/offline_download/http/client.go +++ b/internal/offline_download/http/client.go @@ -49,10 +49,6 @@ func (s SimpleHttp) Status(task *tool.DownloadTask) (*tool.Status, error) { } func (s SimpleHttp) Run(task *tool.DownloadTask) error { - if err := tool.ValidateOfflineDownloadURL(task.Ctx(), task.Url); err != nil { - return err - } - streamPut := task.DeletePolicy == tool.UploadDownloadStream method := http.MethodGet if streamPut { @@ -66,8 +62,7 @@ func (s SimpleHttp) Run(task *tool.DownloadTask) error { if streamPut { req.Header.Set("Range", "bytes=0-") } - client := tool.NewOfflineDownloadHTTPClient(s.client) - resp, err := client.Do(req) + resp, err := s.client.Do(req) if err != nil { return err } diff --git a/internal/offline_download/tool/add.go b/internal/offline_download/tool/add.go index 93be0211a..33128ccc2 100644 --- a/internal/offline_download/tool/add.go +++ b/internal/offline_download/tool/add.go @@ -46,10 +46,6 @@ type AddURLArgs struct { } func AddURL(ctx context.Context, args *AddURLArgs) (task.TaskExtensionInfo, error) { - if err := ValidateOfflineDownloadURL(ctx, args.URL); err != nil { - return nil, err - } - // check storage storage, dstDirActualPath, err := op.GetStorageAndActualPath(args.DstDirPath) if err != nil { diff --git a/internal/offline_download/tool/metadata_url.go b/internal/offline_download/tool/metadata_url.go deleted file mode 100644 index 5d76a6dbd..000000000 --- a/internal/offline_download/tool/metadata_url.go +++ /dev/null @@ -1,91 +0,0 @@ -package tool - -import ( - "context" - "errors" - "net" - "net/http" - "net/url" - "strings" -) - -var ( - ErrCloudMetadataEndpoint = errors.New("access to cloud metadata endpoint is not allowed") - lookupIPAddr = net.DefaultResolver.LookupIPAddr -) - -func ValidateOfflineDownloadURL(ctx context.Context, rawURL string) error { - rawURL = strings.TrimSpace(rawURL) - if rawURL == "" { - return nil - } - if ctx == nil { - ctx = context.Background() - } - - u, err := url.Parse(rawURL) - if err == nil && u.Host != "" { - return validateOfflineDownloadHost(ctx, u.Hostname()) - } - - if err == nil && u.Scheme != "" { - return nil - } - - // Keep scheme-less URLs compatible: only reject direct metadata IP forms here, - // instead of treating any leading path segment as a hostname and doing DNS. - host := strings.Trim(rawURL, "[]") - host, _, _ = strings.Cut(host, "/") - host, _, _ = strings.Cut(host, "?") - host, _, _ = strings.Cut(host, "#") - if splitHost, _, err := net.SplitHostPort(host); err == nil { - host = splitHost - } - if ip := net.ParseIP(host); isCloudMetadataIP(ip) { - return ErrCloudMetadataEndpoint - } - return nil -} - -func NewOfflineDownloadHTTPClient(base http.Client) *http.Client { - client := base - previousCheckRedirect := client.CheckRedirect - client.CheckRedirect = func(req *http.Request, via []*http.Request) error { - if err := ValidateOfflineDownloadURL(req.Context(), req.URL.String()); err != nil { - return err - } - if previousCheckRedirect != nil { - return previousCheckRedirect(req, via) - } - if len(via) >= 10 { - return errors.New("stopped after 10 redirects") - } - return nil - } - return &client -} - -func validateOfflineDownloadHost(ctx context.Context, host string) error { - if ip := net.ParseIP(host); ip != nil { - if isCloudMetadataIP(ip) { - return ErrCloudMetadataEndpoint - } - return nil - } - - addrs, err := lookupIPAddr(ctx, host) - if err != nil { - return err - } - for _, addr := range addrs { - if isCloudMetadataIP(addr.IP) { - return ErrCloudMetadataEndpoint - } - } - return nil -} - -func isCloudMetadataIP(ip net.IP) bool { - ip = ip.To4() - return ip != nil && ip[0] == 169 && ip[1] == 254 && ip[2] == 169 && ip[3] == 254 -} diff --git a/internal/offline_download/tool/metadata_url_test.go b/internal/offline_download/tool/metadata_url_test.go deleted file mode 100644 index 0bd56adc4..000000000 --- a/internal/offline_download/tool/metadata_url_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package tool - -import ( - "context" - "errors" - "net" - "net/http" - "testing" -) - -func TestValidateOfflineDownloadURLRejectsCloudMetadataIP(t *testing.T) { - err := ValidateOfflineDownloadURL(context.Background(), "http://169.254.169.254/") - if !errors.Is(err, ErrCloudMetadataEndpoint) { - t.Fatalf("expected cloud metadata error, got %v", err) - } -} - -func TestValidateOfflineDownloadURLRejectsCloudMetadataIPWithoutScheme(t *testing.T) { - err := ValidateOfflineDownloadURL(context.Background(), "169.254.169.254") - if !errors.Is(err, ErrCloudMetadataEndpoint) { - t.Fatalf("expected cloud metadata error, got %v", err) - } -} - -func TestValidateOfflineDownloadURLRejectsCloudMetadataIPWithPort(t *testing.T) { - err := ValidateOfflineDownloadURL(context.Background(), "http://169.254.169.254:80/") - if !errors.Is(err, ErrCloudMetadataEndpoint) { - t.Fatalf("expected cloud metadata error, got %v", err) - } -} - -func TestValidateOfflineDownloadURLAllowsPublicURL(t *testing.T) { - err := ValidateOfflineDownloadURL(context.Background(), "http://8.8.8.8/") - if err != nil { - t.Fatalf("expected public URL to be allowed, got %v", err) - } -} - -func TestValidateOfflineDownloadURLAllowsPrivateURL(t *testing.T) { - err := ValidateOfflineDownloadURL(context.Background(), "http://192.168.1.10:8080/") - if err != nil { - t.Fatalf("expected private URL to be allowed, got %v", err) - } -} - -func TestValidateOfflineDownloadURLRejectsDomainResolvingToCloudMetadataIP(t *testing.T) { - previousLookup := lookupIPAddr - lookupIPAddr = func(ctx context.Context, host string) ([]net.IPAddr, error) { - if host != "metadata.example.test" { - t.Fatalf("unexpected host lookup: %s", host) - } - return []net.IPAddr{{IP: net.ParseIP("169.254.169.254")}}, nil - } - defer func() { - lookupIPAddr = previousLookup - }() - - err := ValidateOfflineDownloadURL(context.Background(), "http://metadata.example.test/") - if !errors.Is(err, ErrCloudMetadataEndpoint) { - t.Fatalf("expected cloud metadata error, got %v", err) - } -} - -func TestOfflineDownloadHTTPClientRejectsRedirectToCloudMetadataIP(t *testing.T) { - client := NewOfflineDownloadHTTPClient(http.Client{}) - req, err := http.NewRequest(http.MethodGet, "http://169.254.169.254/latest/meta-data/", nil) - if err != nil { - t.Fatalf("failed to build redirect request: %v", err) - } - err = client.CheckRedirect(req, nil) - if !errors.Is(err, ErrCloudMetadataEndpoint) { - t.Fatalf("expected cloud metadata error, got %v", err) - } -} diff --git a/internal/offline_download/transmission/client.go b/internal/offline_download/transmission/client.go index 100e7af5e..f86390fb8 100644 --- a/internal/offline_download/transmission/client.go +++ b/internal/offline_download/transmission/client.go @@ -74,10 +74,6 @@ func (t *Transmission) IsReady() bool { } func (t *Transmission) AddURL(args *tool.AddUrlArgs) (string, error) { - if err := tool.ValidateOfflineDownloadURL(context.Background(), args.Url); err != nil { - return "", err - } - endpoint, err := url.Parse(args.Url) if err != nil { return "", errors.Wrap(err, "failed to parse transmission uri") @@ -88,11 +84,7 @@ func (t *Transmission) AddURL(args *tool.AddUrlArgs) (string, error) { } // http url for .torrent file if endpoint.Scheme == "http" || endpoint.Scheme == "https" { - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, args.Url, nil) - if err != nil { - return "", err - } - resp, err := tool.NewOfflineDownloadHTTPClient(http.Client{}).Do(req) + resp, err := http.Get(args.Url) if err != nil { return "", errors.Wrap(err, "failed to get .torrent file") } From a6adf1049206999f2874401949fa18d25075aad2 Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Mon, 1 Jun 2026 02:17:45 +0800 Subject: [PATCH 3/5] Propagate context to offline download tasks Propagates provided context through the offline download workflow instead of using context.Background(). --- internal/offline_download/115/client.go | 8 +++----- internal/offline_download/115_open/client.go | 8 +++----- internal/offline_download/123/client.go | 7 +++---- internal/offline_download/123_open/client.go | 7 +++---- internal/offline_download/http/client.go | 4 ++-- internal/offline_download/pikpak/pikpak.go | 8 +++----- internal/offline_download/thunder/thunder.go | 8 +++----- .../thunder_browser/thunder_browser.go | 10 ++++------ internal/offline_download/thunderx/thunderx.go | 8 +++----- internal/offline_download/tool/base.go | 3 +++ internal/offline_download/tool/download.go | 1 + internal/offline_download/transmission/client.go | 9 ++++++++- 12 files changed, 39 insertions(+), 42 deletions(-) diff --git a/internal/offline_download/115/client.go b/internal/offline_download/115/client.go index 9e9f702d5..d31971d7c 100644 --- a/internal/offline_download/115/client.go +++ b/internal/offline_download/115/client.go @@ -62,18 +62,16 @@ func (p *Cloud115) AddURL(args *tool.AddUrlArgs) (string, error) { return "", fmt.Errorf("unsupported storage driver for offline download, only 115 Cloud is supported") } - ctx := context.Background() - - if err := op.MakeDir(ctx, storage, actualPath); err != nil { + if err := op.MakeDir(args.Ctx, storage, actualPath); err != nil { return "", err } - parentDir, err := op.GetUnwrap(ctx, storage, actualPath) + parentDir, err := op.GetUnwrap(args.Ctx, storage, actualPath) if err != nil { return "", err } - hashs, err := driver115.OfflineDownload(ctx, []string{args.Url}, parentDir) + hashs, err := driver115.OfflineDownload(args.Ctx, []string{args.Url}, parentDir) if err != nil || len(hashs) < 1 { return "", fmt.Errorf("failed to add offline download task: %w", err) } diff --git a/internal/offline_download/115_open/client.go b/internal/offline_download/115_open/client.go index d12e02ec5..6f4c7fd04 100644 --- a/internal/offline_download/115_open/client.go +++ b/internal/offline_download/115_open/client.go @@ -58,18 +58,16 @@ func (o *Open115) AddURL(args *tool.AddUrlArgs) (string, error) { return "", fmt.Errorf("unsupported storage driver for offline download, only 115 Cloud is supported") } - ctx := context.Background() - - if err := op.MakeDir(ctx, storage, actualPath); err != nil { + if err := op.MakeDir(args.Ctx, storage, actualPath); err != nil { return "", err } - parentDir, err := op.GetUnwrap(ctx, storage, actualPath) + parentDir, err := op.GetUnwrap(args.Ctx, storage, actualPath) if err != nil { return "", err } - hashs, err := driver115Open.OfflineDownload(ctx, []string{args.Url}, parentDir) + hashs, err := driver115Open.OfflineDownload(args.Ctx, []string{args.Url}, parentDir) if err != nil || len(hashs) < 1 { return "", fmt.Errorf("failed to add offline download task: %w", err) } diff --git a/internal/offline_download/123/client.go b/internal/offline_download/123/client.go index 2c4f47048..613860890 100644 --- a/internal/offline_download/123/client.go +++ b/internal/offline_download/123/client.go @@ -58,15 +58,14 @@ func (*Pan123) AddURL(args *tool.AddUrlArgs) (string, error) { if !ok { return "", fmt.Errorf("unsupported storage driver for offline download, only 123Pan is supported") } - ctx := context.Background() - if err := op.MakeDir(ctx, storage, actualPath); err != nil { + if err := op.MakeDir(args.Ctx, storage, actualPath); err != nil { return "", err } - parentDir, err := op.GetUnwrap(ctx, storage, actualPath) + parentDir, err := op.GetUnwrap(args.Ctx, storage, actualPath) if err != nil { return "", err } - taskID, err := driver123.OfflineDownload(ctx, args.Url, parentDir) + taskID, err := driver123.OfflineDownload(args.Ctx, args.Url, parentDir) if err != nil { return "", fmt.Errorf("failed to add offline download task: %w", err) } diff --git a/internal/offline_download/123_open/client.go b/internal/offline_download/123_open/client.go index ce1453c32..997eeabef 100644 --- a/internal/offline_download/123_open/client.go +++ b/internal/offline_download/123_open/client.go @@ -56,16 +56,15 @@ func (*Open123) AddURL(args *tool.AddUrlArgs) (string, error) { if !ok { return "", fmt.Errorf("unsupported storage driver for offline download, only 123 Open is supported") } - ctx := context.Background() - if err := op.MakeDir(ctx, storage, actualPath); err != nil { + if err := op.MakeDir(args.Ctx, storage, actualPath); err != nil { return "", err } - parentDir, err := op.GetUnwrap(ctx, storage, actualPath) + parentDir, err := op.GetUnwrap(args.Ctx, storage, actualPath) if err != nil { return "", err } cb := setting.GetStr(conf.Pan123OpenOfflineDownloadCallbackUrl) - taskID, err := driver123Open.OfflineDownload(ctx, args.Url, parentDir, cb) + taskID, err := driver123Open.OfflineDownload(args.Ctx, args.Url, parentDir, cb) if err != nil { return "", fmt.Errorf("failed to add offline download task: %w", err) } diff --git a/internal/offline_download/http/client.go b/internal/offline_download/http/client.go index 86314031d..6b3c45a5f 100644 --- a/internal/offline_download/http/client.go +++ b/internal/offline_download/http/client.go @@ -11,13 +11,13 @@ import ( "github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/net" "github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool" "github.com/OpenListTeam/OpenList/v4/pkg/http_range" "github.com/OpenListTeam/OpenList/v4/pkg/utils" ) type SimpleHttp struct { - client http.Client } func (s SimpleHttp) Name() string { @@ -62,7 +62,7 @@ func (s SimpleHttp) Run(task *tool.DownloadTask) error { if streamPut { req.Header.Set("Range", "bytes=0-") } - resp, err := s.client.Do(req) + resp, err := net.HttpClient().Do(req) if err != nil { return err } diff --git a/internal/offline_download/pikpak/pikpak.go b/internal/offline_download/pikpak/pikpak.go index f48ed9951..8a6693091 100644 --- a/internal/offline_download/pikpak/pikpak.go +++ b/internal/offline_download/pikpak/pikpak.go @@ -63,18 +63,16 @@ func (p *PikPak) AddURL(args *tool.AddUrlArgs) (string, error) { return "", fmt.Errorf("unsupported storage driver for offline download, only Pikpak is supported") } - ctx := context.Background() - - if err := op.MakeDir(ctx, storage, actualPath); err != nil { + if err := op.MakeDir(args.Ctx, storage, actualPath); err != nil { return "", err } - parentDir, err := op.GetUnwrap(ctx, storage, actualPath) + parentDir, err := op.GetUnwrap(args.Ctx, storage, actualPath) if err != nil { return "", err } - t, err := pikpakDriver.OfflineDownload(ctx, args.Url, parentDir, "") + t, err := pikpakDriver.OfflineDownload(args.Ctx, args.Url, parentDir, "") if err != nil { return "", fmt.Errorf("failed to add offline download task: %w", err) } diff --git a/internal/offline_download/thunder/thunder.go b/internal/offline_download/thunder/thunder.go index 7cbff89df..1a134f34a 100644 --- a/internal/offline_download/thunder/thunder.go +++ b/internal/offline_download/thunder/thunder.go @@ -64,18 +64,16 @@ func (t *Thunder) AddURL(args *tool.AddUrlArgs) (string, error) { return "", fmt.Errorf("unsupported storage driver for offline download, only Thunder is supported") } - ctx := context.Background() - - if err := op.MakeDir(ctx, storage, actualPath); err != nil { + if err := op.MakeDir(args.Ctx, storage, actualPath); err != nil { return "", err } - parentDir, err := op.GetUnwrap(ctx, storage, actualPath) + parentDir, err := op.GetUnwrap(args.Ctx, storage, actualPath) if err != nil { return "", err } - task, err := thunderDriver.OfflineDownload(ctx, args.Url, parentDir, "") + task, err := thunderDriver.OfflineDownload(args.Ctx, args.Url, parentDir, "") if err != nil { return "", fmt.Errorf("failed to add offline download task: %w", err) } diff --git a/internal/offline_download/thunder_browser/thunder_browser.go b/internal/offline_download/thunder_browser/thunder_browser.go index 9324d7a76..d1c767122 100644 --- a/internal/offline_download/thunder_browser/thunder_browser.go +++ b/internal/offline_download/thunder_browser/thunder_browser.go @@ -63,13 +63,11 @@ func (t *ThunderBrowser) AddURL(args *tool.AddUrlArgs) (string, error) { return "", err } - ctx := context.Background() - - if err := op.MakeDir(ctx, storage, actualPath); err != nil { + if err := op.MakeDir(args.Ctx, storage, actualPath); err != nil { return "", err } - parentDir, err := op.GetUnwrap(ctx, storage, actualPath) + parentDir, err := op.GetUnwrap(args.Ctx, storage, actualPath) if err != nil { return "", err } @@ -77,9 +75,9 @@ func (t *ThunderBrowser) AddURL(args *tool.AddUrlArgs) (string, error) { var task *thunder_browser.OfflineTask switch v := storage.(type) { case *thunder_browser.ThunderBrowser: - task, err = v.OfflineDownload(ctx, args.Url, parentDir, "") + task, err = v.OfflineDownload(args.Ctx, args.Url, parentDir, "") case *thunder_browser.ThunderBrowserExpert: - task, err = v.OfflineDownload(ctx, args.Url, parentDir, "") + task, err = v.OfflineDownload(args.Ctx, args.Url, parentDir, "") default: return "", fmt.Errorf("unsupported storage driver for offline download, only ThunderBrowser is supported") } diff --git a/internal/offline_download/thunderx/thunderx.go b/internal/offline_download/thunderx/thunderx.go index 3ba4a3680..9ba4c7eb8 100644 --- a/internal/offline_download/thunderx/thunderx.go +++ b/internal/offline_download/thunderx/thunderx.go @@ -58,18 +58,16 @@ func (t *ThunderX) AddURL(args *tool.AddUrlArgs) (string, error) { return "", fmt.Errorf("unsupported storage driver for offline download, only ThunderX is supported") } - ctx := context.Background() - - if err := op.MakeDir(ctx, storage, actualPath); err != nil { + if err := op.MakeDir(args.Ctx, storage, actualPath); err != nil { return "", err } - parentDir, err := op.GetUnwrap(ctx, storage, actualPath) + parentDir, err := op.GetUnwrap(args.Ctx, storage, actualPath) if err != nil { return "", err } - task, err := thunderXDriver.OfflineDownload(ctx, args.Url, parentDir, "") + task, err := thunderXDriver.OfflineDownload(args.Ctx, args.Url, parentDir, "") if err != nil { return "", fmt.Errorf("failed to add offline download task: %w", err) } diff --git a/internal/offline_download/tool/base.go b/internal/offline_download/tool/base.go index 6de05e5b2..823bac526 100644 --- a/internal/offline_download/tool/base.go +++ b/internal/offline_download/tool/base.go @@ -1,6 +1,8 @@ package tool import ( + "context" + "github.com/OpenListTeam/OpenList/v4/internal/model" ) @@ -9,6 +11,7 @@ type AddUrlArgs struct { UID string TempDir string Signal chan int + Ctx context.Context } type Status struct { diff --git a/internal/offline_download/tool/download.go b/internal/offline_download/tool/download.go index 5ee6ef4ff..b7f7a1a9d 100644 --- a/internal/offline_download/tool/download.go +++ b/internal/offline_download/tool/download.go @@ -54,6 +54,7 @@ func (t *DownloadTask) Run() error { t.Signal = nil }() gid, err := t.tool.AddURL(&AddUrlArgs{ + Ctx: t.Ctx(), Url: t.Url, UID: t.ID, TempDir: t.TempDir, diff --git a/internal/offline_download/transmission/client.go b/internal/offline_download/transmission/client.go index f86390fb8..013180e46 100644 --- a/internal/offline_download/transmission/client.go +++ b/internal/offline_download/transmission/client.go @@ -9,9 +9,11 @@ import ( "net/url" "strconv" + "github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/internal/conf" "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/net" "github.com/OpenListTeam/OpenList/v4/internal/offline_download/tool" "github.com/OpenListTeam/OpenList/v4/internal/setting" "github.com/OpenListTeam/OpenList/v4/pkg/utils" @@ -84,7 +86,12 @@ func (t *Transmission) AddURL(args *tool.AddUrlArgs) (string, error) { } // http url for .torrent file if endpoint.Scheme == "http" || endpoint.Scheme == "https" { - resp, err := http.Get(args.Url) + resp, err := net.RequestHttp( + args.Ctx, + http.MethodGet, + http.Header{"User-Agent": []string{base.UserAgent}}, + args.Url, + ) if err != nil { return "", errors.Wrap(err, "failed to get .torrent file") } From ad46c9731d1a99843cf17401ac367cda44a12253 Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Mon, 1 Jun 2026 14:55:52 +0800 Subject: [PATCH 4/5] fix(net): Blocks cloud metadata endpoint Adds a safe HTTP transport wrapper that resolves request hosts and rejects requests to the cloud metadata IP (169.254.169.254), returning a clear error. Integrates the wrapper into the default HTTP client to prevent accidental or malicious access to instance metadata and mitigate SSRF/exfiltration risks. --- internal/net/serve.go | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/internal/net/serve.go b/internal/net/serve.go index 6a20460b1..03ae52b47 100644 --- a/internal/net/serve.go +++ b/internal/net/serve.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "mime/multipart" + gonet "net" "net/http" "strconv" "strings" @@ -292,6 +293,34 @@ func NewHttpClient() *http.Client { return &http.Client{ Timeout: time.Hour * 48, - Transport: transport, + Transport: &safeTransport{base: transport}, } } + +type safeTransport struct { + base http.RoundTripper +} + +func (t *safeTransport) RoundTrip(req *http.Request) (*http.Response, error) { + host := req.URL.Hostname() + addrs, err := gonet.DefaultResolver.LookupIPAddr(context.Background(), host) + if err != nil { + return nil, errors.Wrapf(err, "failed to resolve host: %s", host) + } + if len(addrs) == 0 { + return nil, fmt.Errorf("no IP addresses found for host: %s", host) + } + for _, addr := range addrs { + if isCloudMetadataIP(addr.IP) { + return nil, ErrCloudMetadataEndpoint + } + } + return t.base.RoundTrip(req) +} + +var ErrCloudMetadataEndpoint = errors.New("access to cloud metadata endpoint is not allowed") + +func isCloudMetadataIP(ip gonet.IP) bool { + ip = ip.To4() + return ip != nil && ip[0] == 169 && ip[1] == 254 && ip[2] == 169 && ip[3] == 254 +} From cbcc09838624ffd93e1595752ef05623fbdf5d8d Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Mon, 1 Jun 2026 17:18:03 +0800 Subject: [PATCH 5/5] fix: Propagates contexts to DNS and client calls --- internal/net/serve.go | 7 ++----- internal/offline_download/transmission/client.go | 6 +++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/internal/net/serve.go b/internal/net/serve.go index 03ae52b47..89a209d88 100644 --- a/internal/net/serve.go +++ b/internal/net/serve.go @@ -303,13 +303,10 @@ type safeTransport struct { func (t *safeTransport) RoundTrip(req *http.Request) (*http.Response, error) { host := req.URL.Hostname() - addrs, err := gonet.DefaultResolver.LookupIPAddr(context.Background(), host) - if err != nil { + addrs, err := gonet.DefaultResolver.LookupIPAddr(req.Context(), host) + if err != nil || len(addrs) == 0 { return nil, errors.Wrapf(err, "failed to resolve host: %s", host) } - if len(addrs) == 0 { - return nil, fmt.Errorf("no IP addresses found for host: %s", host) - } for _, addr := range addrs { if isCloudMetadataIP(addr.IP) { return nil, ErrCloudMetadataEndpoint diff --git a/internal/offline_download/transmission/client.go b/internal/offline_download/transmission/client.go index 013180e46..49e48aa68 100644 --- a/internal/offline_download/transmission/client.go +++ b/internal/offline_download/transmission/client.go @@ -113,7 +113,7 @@ func (t *Transmission) AddURL(args *tool.AddUrlArgs) (string, error) { rpcPayload.Filename = &args.Url } - torrent, err := t.client.TorrentAdd(context.TODO(), rpcPayload) + torrent, err := t.client.TorrentAdd(args.Ctx, rpcPayload) if err != nil { return "", err } @@ -130,7 +130,7 @@ func (t *Transmission) Remove(task *tool.DownloadTask) error { if err != nil { return err } - err = t.client.TorrentRemove(context.TODO(), transmissionrpc.TorrentRemovePayload{ + err = t.client.TorrentRemove(task.Ctx(), transmissionrpc.TorrentRemovePayload{ IDs: []int64{gid}, DeleteLocalData: false, }) @@ -142,7 +142,7 @@ func (t *Transmission) Status(task *tool.DownloadTask) (*tool.Status, error) { if err != nil { return nil, err } - infos, err := t.client.TorrentGetAllFor(context.TODO(), []int64{gid}) + infos, err := t.client.TorrentGetAllFor(task.Ctx(), []int64{gid}) if err != nil { return nil, err }