From 638e4c8995d1f894097331e419d83ab935aec4ec Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Fri, 21 Nov 2025 16:05:40 +0100 Subject: [PATCH 01/16] fix: Resolve URL path before fetching content This prevents from submitting requests to URL containing any `../` that could trigger security proxies. Signed-off-by: Paul Mars --- internal/archive/archive.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 4e0a2dea..a85646de 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -378,14 +378,16 @@ func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.Rea baseURL, creds := index.archive.baseURL, index.archive.creds - var url string - if strings.HasPrefix(suffix, "pool/") { - url = baseURL + suffix - } else { - url = baseURL + "dists/" + index.suite + "/" + suffix + if !strings.HasPrefix(suffix, "pool/") { + suffix = "dists/" + index.suite + "/" + suffix + } + + u, err := url.JoinPath(baseURL, suffix) + if err != nil { + return nil, fmt.Errorf("cannot construct URL: %v", err) } - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequest("GET", u, nil) if err != nil { return nil, fmt.Errorf("cannot create HTTP request: %v", err) } From 24dc87fcb2ccdc3e334089f3e0bb20a68f33c073 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Fri, 21 Nov 2025 16:09:20 +0100 Subject: [PATCH 02/16] fix: missing import Signed-off-by: Paul Mars --- internal/archive/archive.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index a85646de..51fe27c6 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "net/url" "slices" "strings" "time" From 183f188e1fa1f5f90940e89364dd4220cc4f4519 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Fri, 21 Nov 2025 17:38:13 +0100 Subject: [PATCH 03/16] test: URL cleaning Signed-off-by: Paul Mars --- internal/archive/archive_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index 1671e79a..01daa441 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -72,6 +72,14 @@ func (s *httpSuite) Do(req *http.Request) (*http.Response, error) { return nil, fmt.Errorf("test expected base %q, got %q", s.base, req.URL.String()) } + cleanURL, err := url.JoinPath(req.URL.String()) + if err != nil { + return nil, fmt.Errorf("cannot clean requested URL: %v", err) + } + if cleanURL != req.URL.String() { + return nil, fmt.Errorf("test expected a clean URL %s, got %q", cleanURL, req.URL.String()) + } + s.request = req s.requests = append(s.requests, req) body := s.response From 0c2633d2d58aa5271e10d817cba96b380aa56b41 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Mon, 24 Nov 2025 17:20:01 +0100 Subject: [PATCH 04/16] fix: mark error as internal Signed-off-by: Paul Mars --- internal/archive/archive.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 51fe27c6..6e803966 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -385,7 +385,7 @@ func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.Rea u, err := url.JoinPath(baseURL, suffix) if err != nil { - return nil, fmt.Errorf("cannot construct URL: %v", err) + return nil, fmt.Errorf("internal error: cannot construct URL: %v", err) } req, err := http.NewRequest("GET", u, nil) From 7b84848628beb339a83e4f31b005f4918797f47b Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Mon, 24 Nov 2025 17:21:24 +0100 Subject: [PATCH 05/16] style: improve naming Signed-off-by: Paul Mars --- internal/archive/archive.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 6e803966..8020bc33 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -383,12 +383,12 @@ func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.Rea suffix = "dists/" + index.suite + "/" + suffix } - u, err := url.JoinPath(baseURL, suffix) + reqURL, err := url.JoinPath(baseURL, suffix) if err != nil { return nil, fmt.Errorf("internal error: cannot construct URL: %v", err) } - req, err := http.NewRequest("GET", u, nil) + req, err := http.NewRequest("GET", reqURL, nil) if err != nil { return nil, fmt.Errorf("cannot create HTTP request: %v", err) } From 1fbc90251b82aa0c47bdf38b3e3582c8b7f18934 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Mon, 24 Nov 2025 17:26:47 +0100 Subject: [PATCH 06/16] fix(test): improve error message formatting Signed-off-by: Paul Mars --- internal/archive/archive_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index 01daa441..fb84ee6e 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -77,7 +77,7 @@ func (s *httpSuite) Do(req *http.Request) (*http.Response, error) { return nil, fmt.Errorf("cannot clean requested URL: %v", err) } if cleanURL != req.URL.String() { - return nil, fmt.Errorf("test expected a clean URL %s, got %q", cleanURL, req.URL.String()) + return nil, fmt.Errorf("test expected a clean URL %q, got %q", cleanURL, req.URL.String()) } s.request = req From 57cef9f53e7ff135cef023d8d3a17049a0fb0148 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Tue, 25 Nov 2025 12:04:09 +0100 Subject: [PATCH 07/16] docs: clarify suffix handling Signed-off-by: Paul Mars --- internal/archive/archive.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 8020bc33..b71d6d87 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -379,6 +379,7 @@ func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.Rea baseURL, creds := index.archive.baseURL, index.archive.creds + // Scope content fetching with the suite unless fetching a package from the pool if !strings.HasPrefix(suffix, "pool/") { suffix = "dists/" + index.suite + "/" + suffix } From 31a78dcd4cff61aec8d40315b195204044a465af Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Tue, 25 Nov 2025 12:07:11 +0100 Subject: [PATCH 08/16] refactor: simplify fetching package Do not use `../../` in the path. It was trying to manipulate the behavior of `fetch()`, making assumptions on its implementation. Signed-off-by: Paul Mars --- internal/archive/archive.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index b71d6d87..7852d056 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -137,7 +137,7 @@ func (a *ubuntuArchive) Fetch(pkg string) (io.ReadSeekCloser, *PackageInfo, erro } suffix := section.Get("Filename") logf("Fetching %s...", suffix) - reader, err := index.fetch("../../"+suffix, section.Get("SHA256"), fetchBulk) + reader, err := index.fetch(suffix, section.Get("SHA256"), fetchBulk) if err != nil { return nil, nil, err } From a8a06ee1da9ec746723447108436708f054fe8c7 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Tue, 25 Nov 2025 13:35:27 +0100 Subject: [PATCH 09/16] refactor: revert some changes to minimize diff Signed-off-by: Paul Mars --- internal/archive/archive.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 7852d056..d79ccc27 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "net/http" - "net/url" "slices" "strings" "time" @@ -379,17 +378,15 @@ func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.Rea baseURL, creds := index.archive.baseURL, index.archive.creds - // Scope content fetching with the suite unless fetching a package from the pool - if !strings.HasPrefix(suffix, "pool/") { - suffix = "dists/" + index.suite + "/" + suffix - } - - reqURL, err := url.JoinPath(baseURL, suffix) - if err != nil { - return nil, fmt.Errorf("internal error: cannot construct URL: %v", err) + var url string + if strings.HasPrefix(suffix, "pool/") { + url = baseURL + suffix + } else { + // If path is not a package then it is relative to the suite. + url = baseURL + "dists/" + index.suite + "/" + suffix } - req, err := http.NewRequest("GET", reqURL, nil) + req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("cannot create HTTP request: %v", err) } From c4e2c6689b2472c5d467e5e2f76916553754dddb Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 7 Jan 2026 11:40:51 +0100 Subject: [PATCH 10/16] style: clearer test error message --- internal/archive/archive_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/archive/archive_test.go b/internal/archive/archive_test.go index fb84ee6e..8ca840e5 100644 --- a/internal/archive/archive_test.go +++ b/internal/archive/archive_test.go @@ -77,7 +77,7 @@ func (s *httpSuite) Do(req *http.Request) (*http.Response, error) { return nil, fmt.Errorf("cannot clean requested URL: %v", err) } if cleanURL != req.URL.String() { - return nil, fmt.Errorf("test expected a clean URL %q, got %q", cleanURL, req.URL.String()) + return nil, fmt.Errorf("test expected clean URL %q, got %q", cleanURL, req.URL.String()) } s.request = req From f9f7a9e2a210c2ae17d9e36259e4f6f324fee61a Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Mon, 12 Jan 2026 15:05:16 +0100 Subject: [PATCH 11/16] style: remove redondant comment --- internal/archive/archive.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index d79ccc27..c5f10596 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -382,7 +382,6 @@ func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.Rea if strings.HasPrefix(suffix, "pool/") { url = baseURL + suffix } else { - // If path is not a package then it is relative to the suite. url = baseURL + "dists/" + index.suite + "/" + suffix } From 9cd44372bf552457ad79af74ff96ab4f864fa93d Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Tue, 13 Jan 2026 11:48:33 +0100 Subject: [PATCH 12/16] refactor: do not assume the archive structure --- internal/archive/archive.go | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index c5f10596..4a23c186 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -134,9 +134,9 @@ func (a *ubuntuArchive) Fetch(pkg string) (io.ReadSeekCloser, *PackageInfo, erro if err != nil { return nil, nil, err } - suffix := section.Get("Filename") - logf("Fetching %s...", suffix) - reader, err := index.fetch(suffix, section.Get("SHA256"), fetchBulk) + path := section.Get("Filename") + logf("Fetching %s...", path) + reader, err := index.fetch(path, section.Get("SHA256"), fetchBulk) if err != nil { return nil, nil, err } @@ -276,7 +276,7 @@ func openUbuntu(options *Options) (Archive, error) { func (index *ubuntuIndex) fetchRelease() error { logf("Fetching %s %s %s suite details...", index.displayName(), index.version, index.suite) - reader, err := index.fetch("InRelease", "", fetchDefault) + reader, err := index.fetch(index.relativePath("InRelease"), "", fetchDefault) if err != nil { return err } @@ -333,7 +333,7 @@ func (index *ubuntuIndex) fetchIndex() error { } logf("Fetching index for %s %s %s %s component...", index.displayName(), index.version, index.suite, index.component) - reader, err := index.fetch(packagesPath+".gz", digest, fetchBulk) + reader, err := index.fetch(index.relativePath(packagesPath+".gz"), digest, fetchBulk) if err != nil { return err } @@ -368,7 +368,11 @@ func (index *ubuntuIndex) checkComponents(components []string) error { return nil } -func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.ReadSeekCloser, error) { +func (index *ubuntuIndex) relativePath(suffix string) string { + return "dists/" + index.suite + "/" + suffix +} + +func (index *ubuntuIndex) fetch(path, digest string, flags fetchFlags) (io.ReadSeekCloser, error) { reader, err := index.archive.cache.Open(digest) if err == nil { return reader, nil @@ -376,19 +380,11 @@ func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.Rea return nil, err } - baseURL, creds := index.archive.baseURL, index.archive.creds - - var url string - if strings.HasPrefix(suffix, "pool/") { - url = baseURL + suffix - } else { - url = baseURL + "dists/" + index.suite + "/" + suffix - } - - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequest("GET", index.archive.baseURL + path, nil) if err != nil { return nil, fmt.Errorf("cannot create HTTP request: %v", err) } + creds := index.archive.creds if creds != nil && !creds.Empty() { req.SetBasicAuth(creds.Username, creds.Password) } @@ -415,7 +411,7 @@ func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.Rea } body := resp.Body - if strings.HasSuffix(suffix, ".gz") { + if strings.HasSuffix(path, ".gz") { reader, err := gzip.NewReader(body) if err != nil { return nil, fmt.Errorf("cannot decompress data: %v", err) From 073443c7ff55222832716c2c8bfd32c8588d4e10 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Tue, 13 Jan 2026 11:56:16 +0100 Subject: [PATCH 13/16] style: fix formatting --- internal/archive/archive.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 4a23c186..dfc953b3 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -380,7 +380,7 @@ func (index *ubuntuIndex) fetch(path, digest string, flags fetchFlags) (io.ReadS return nil, err } - req, err := http.NewRequest("GET", index.archive.baseURL + path, nil) + req, err := http.NewRequest("GET", index.archive.baseURL+path, nil) if err != nil { return nil, fmt.Errorf("cannot create HTTP request: %v", err) } From 09c9b10ceb30f3d79d165db1cab62774e2f2a8cd Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Tue, 13 Jan 2026 14:11:31 +0100 Subject: [PATCH 14/16] style: improving name to get the dist path --- internal/archive/archive.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index dfc953b3..128827c3 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -276,7 +276,7 @@ func openUbuntu(options *Options) (Archive, error) { func (index *ubuntuIndex) fetchRelease() error { logf("Fetching %s %s %s suite details...", index.displayName(), index.version, index.suite) - reader, err := index.fetch(index.relativePath("InRelease"), "", fetchDefault) + reader, err := index.fetch(index.distPath("InRelease"), "", fetchDefault) if err != nil { return err } @@ -333,7 +333,7 @@ func (index *ubuntuIndex) fetchIndex() error { } logf("Fetching index for %s %s %s %s component...", index.displayName(), index.version, index.suite, index.component) - reader, err := index.fetch(index.relativePath(packagesPath+".gz"), digest, fetchBulk) + reader, err := index.fetch(index.distPath(packagesPath+".gz"), digest, fetchBulk) if err != nil { return err } @@ -368,7 +368,7 @@ func (index *ubuntuIndex) checkComponents(components []string) error { return nil } -func (index *ubuntuIndex) relativePath(suffix string) string { +func (index *ubuntuIndex) distPath(suffix string) string { return "dists/" + index.suite + "/" + suffix } From 938e05de639a6ff6517e8d722bf2e4e007f61a59 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Wed, 14 Jan 2026 11:15:16 +0100 Subject: [PATCH 15/16] ci: retry From e9ccfdab7aa2111cac014f561ec45cf704023310 Mon Sep 17 00:00:00 2001 From: Paul Mars Date: Tue, 20 Jan 2026 11:25:17 +0100 Subject: [PATCH 16/16] fix: construct URL in a robust way --- internal/archive/archive.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 128827c3..e24a9c24 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "net/url" "slices" "strings" "time" @@ -380,7 +381,11 @@ func (index *ubuntuIndex) fetch(path, digest string, flags fetchFlags) (io.ReadS return nil, err } - req, err := http.NewRequest("GET", index.archive.baseURL+path, nil) + cleanURL, err := url.JoinPath(index.archive.baseURL, path) + if err != nil { + return nil, fmt.Errorf("internal error: cannot construct URL: %v", err) + } + req, err := http.NewRequest("GET", cleanURL, nil) if err != nil { return nil, fmt.Errorf("cannot create HTTP request: %v", err) }