diff --git a/internal/cargo/cargo.go b/internal/cargo/cargo.go index 6233606..59eda52 100644 --- a/internal/cargo/cargo.go +++ b/internal/cargo/cargo.go @@ -4,6 +4,7 @@ package cargo import ( "context" "fmt" + "net/url" "strings" "time" @@ -102,7 +103,7 @@ type ownerInfo struct { } func (r *Registry) FetchPackage(ctx context.Context, name string) (*core.Package, error) { - url := fmt.Sprintf("%s/api/v1/crates/%s", r.baseURL, name) + url := fmt.Sprintf("%s/api/v1/crates/%s", r.baseURL, url.PathEscape(name)) var resp crateResponse if err := r.client.GetJSON(ctx, url, &resp); err != nil { @@ -132,7 +133,7 @@ func (r *Registry) FetchPackage(ctx context.Context, name string) (*core.Package } func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Version, error) { - url := fmt.Sprintf("%s/api/v1/crates/%s", r.baseURL, name) + url := fmt.Sprintf("%s/api/v1/crates/%s", r.baseURL, url.PathEscape(name)) var resp crateResponse if err := r.client.GetJSON(ctx, url, &resp); err != nil { @@ -181,7 +182,7 @@ func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Versi } func (r *Registry) FetchDependencies(ctx context.Context, name, version string) ([]core.Dependency, error) { - url := fmt.Sprintf("%s/api/v1/crates/%s/%s/dependencies", r.baseURL, name, version) + url := fmt.Sprintf("%s/api/v1/crates/%s/%s/dependencies", r.baseURL, url.PathEscape(name), url.PathEscape(version)) var resp dependenciesResponse if err := r.client.GetJSON(ctx, url, &resp); err != nil { @@ -216,7 +217,7 @@ func mapScope(kind string) core.Scope { } func (r *Registry) FetchMaintainers(ctx context.Context, name string) ([]core.Maintainer, error) { - url := fmt.Sprintf("%s/api/v1/crates/%s/owner_user", r.baseURL, name) + url := fmt.Sprintf("%s/api/v1/crates/%s/owner_user", r.baseURL, url.PathEscape(name)) var resp ownersResponse if err := r.client.GetJSON(ctx, url, &resp); err != nil { diff --git a/internal/cocoapods/cocoapods.go b/internal/cocoapods/cocoapods.go index 7c9464f..67e55d1 100644 --- a/internal/cocoapods/cocoapods.go +++ b/internal/cocoapods/cocoapods.go @@ -4,6 +4,7 @@ package cocoapods import ( "context" "fmt" + "net/url" "strings" "time" @@ -79,7 +80,7 @@ type ownerInfo struct { } func (r *Registry) FetchPackage(ctx context.Context, name string) (*core.Package, error) { - url := fmt.Sprintf("%s/api/v1/pods/%s", r.baseURL, name) + url := fmt.Sprintf("%s/api/v1/pods/%s", r.baseURL, url.PathEscape(name)) var resp podResponse if err := r.client.GetJSON(ctx, url, &resp); err != nil { @@ -120,7 +121,7 @@ func (r *Registry) FetchPackage(ctx context.Context, name string) (*core.Package } func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Version, error) { - url := fmt.Sprintf("%s/api/v1/pods/%s", r.baseURL, name) + url := fmt.Sprintf("%s/api/v1/pods/%s", r.baseURL, url.PathEscape(name)) var resp podResponse if err := r.client.GetJSON(ctx, url, &resp); err != nil { @@ -143,7 +144,7 @@ func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Versi } func (r *Registry) FetchDependencies(ctx context.Context, name, version string) ([]core.Dependency, error) { - url := fmt.Sprintf("%s/api/v1/pods/%s", r.baseURL, name) + url := fmt.Sprintf("%s/api/v1/pods/%s", r.baseURL, url.PathEscape(name)) var resp podResponse if err := r.client.GetJSON(ctx, url, &resp); err != nil { @@ -195,7 +196,7 @@ func formatRequirement(req interface{}) string { } func (r *Registry) FetchMaintainers(ctx context.Context, name string) ([]core.Maintainer, error) { - url := fmt.Sprintf("%s/api/v1/pods/%s", r.baseURL, name) + url := fmt.Sprintf("%s/api/v1/pods/%s", r.baseURL, url.PathEscape(name)) var resp podResponse if err := r.client.GetJSON(ctx, url, &resp); err != nil { diff --git a/internal/deno/deno.go b/internal/deno/deno.go index c706ffc..daffc2d 100644 --- a/internal/deno/deno.go +++ b/internal/deno/deno.go @@ -4,6 +4,7 @@ package deno import ( "context" "fmt" + "net/url" "strings" "time" @@ -87,7 +88,7 @@ type directoryEntry struct { } func (r *Registry) FetchPackage(ctx context.Context, name string) (*core.Package, error) { - url := fmt.Sprintf("%s/v2/modules/%s", r.baseURL, name) + url := fmt.Sprintf("%s/v2/modules/%s", r.baseURL, url.PathEscape(name)) var resp moduleInfoResponse if err := r.client.GetJSON(ctx, url, &resp); err != nil { @@ -115,7 +116,7 @@ func (r *Registry) FetchPackage(ctx context.Context, name string) (*core.Package } func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Version, error) { - url := fmt.Sprintf("%s/v2/modules/%s", r.baseURL, name) + url := fmt.Sprintf("%s/v2/modules/%s", r.baseURL, url.PathEscape(name)) var resp moduleInfoResponse if err := r.client.GetJSON(ctx, url, &resp); err != nil { diff --git a/internal/pypi/pypi.go b/internal/pypi/pypi.go index 8ad3782..d41fe41 100644 --- a/internal/pypi/pypi.go +++ b/internal/pypi/pypi.go @@ -4,6 +4,7 @@ package pypi import ( "context" "fmt" + "net/url" "regexp" "strings" "time" @@ -86,7 +87,7 @@ type versionInfoResponse struct { } func (r *Registry) FetchPackage(ctx context.Context, name string) (*core.Package, error) { - url := fmt.Sprintf("%s/pypi/%s/json", r.baseURL, name) + url := fmt.Sprintf("%s/pypi/%s/json", r.baseURL, url.PathEscape(name)) var resp packageResponse if err := r.client.GetJSON(ctx, url, &resp); err != nil { @@ -190,7 +191,7 @@ func normalizeName(name string) string { } func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Version, error) { - url := fmt.Sprintf("%s/pypi/%s/json", r.baseURL, name) + url := fmt.Sprintf("%s/pypi/%s/json", r.baseURL, url.PathEscape(name)) var resp packageResponse if err := r.client.GetJSON(ctx, url, &resp); err != nil { @@ -246,7 +247,7 @@ func (r *Registry) FetchVersions(ctx context.Context, name string) ([]core.Versi var pep508NameRegex = regexp.MustCompile(`^([A-Za-z0-9][-A-Za-z0-9._]*[A-Za-z0-9]|[A-Za-z0-9])(\s*\[.*?\])?`) func (r *Registry) FetchDependencies(ctx context.Context, name, version string) ([]core.Dependency, error) { - url := fmt.Sprintf("%s/pypi/%s/%s/json", r.baseURL, name, version) + url := fmt.Sprintf("%s/pypi/%s/%s/json", r.baseURL, url.PathEscape(name), url.PathEscape(version)) var resp versionInfoResponse if err := r.client.GetJSON(ctx, url, &resp); err != nil { diff --git a/internal/pypi/pypi_test.go b/internal/pypi/pypi_test.go index 001cbb4..a258b92 100644 --- a/internal/pypi/pypi_test.go +++ b/internal/pypi/pypi_test.go @@ -65,6 +65,30 @@ func TestFetchPackage(t *testing.T) { } } +func TestFetchPackageEscapesName(t *testing.T) { + var gotPath, gotQuery string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + gotQuery = r.URL.RawQuery + w.WriteHeader(404) + })) + defer server.Close() + + reg := New(server.URL, core.DefaultClient()) + _, _ = reg.FetchPackage(context.Background(), "requests?evil=1#frag") + + // Without escaping the ? starts a query string client-side and the + // server sees path /pypi/requests with query evil=1/json. With + // escaping the whole name is one path segment, the # doesn't + // truncate as a fragment, and /json survives at the end. + if gotQuery != "" { + t.Errorf("query string leaked: %q (path was %q)", gotQuery, gotPath) + } + if gotPath != "/pypi/requests?evil=1#frag/json" { + t.Errorf("path = %q, expected name kept intact with /json suffix", gotPath) + } +} + func TestFetchPackageWithLicenseExpression(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { resp := packageResponse{