1
0
Fork 1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-06-02 00:38:16 -04:00

Compare commits

...

1 commit

Author SHA1 Message Date
Andrew Nesbitt
37cc7abfc7
Validate package paths before database lookups
The wildcard package routes (/packages/{ecosystem}/*, /api/package/*,
/api/vulns/*, /api/browse/*, /api/compare/*) only checked for an empty
path before passing user input to GetPackageByEcosystemName and the
enrichment service.

Add validatePackagePath as a coarse first-line filter: reject null
bytes, other control characters, and paths over 512 bytes. Wired into
all five entry handlers immediately after the chi wildcard is read.

This is the generic layer; ecosystem-specific name format rules (npm
scoped name shape, Maven coordinate structure, etc.) can be added on
top per #75.

Fixes #75
2026-05-03 09:02:14 +01:00
6 changed files with 112 additions and 0 deletions

View file

@ -140,6 +140,10 @@ type BulkResponse struct {
func (h *APIHandler) HandlePackagePath(w http.ResponseWriter, r *http.Request) { func (h *APIHandler) HandlePackagePath(w http.ResponseWriter, r *http.Request) {
ecosystem := chi.URLParam(r, "ecosystem") ecosystem := chi.URLParam(r, "ecosystem")
wildcard := chi.URLParam(r, "*") wildcard := chi.URLParam(r, "*")
if err := validatePackagePath(wildcard); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
segments := splitWildcardPath(wildcard) segments := splitWildcardPath(wildcard)
if ecosystem == "" || len(segments) == 0 { if ecosystem == "" || len(segments) == 0 {
@ -274,6 +278,10 @@ func (h *APIHandler) getVersion(w http.ResponseWriter, r *http.Request, ecosyste
func (h *APIHandler) HandleVulnsPath(w http.ResponseWriter, r *http.Request) { func (h *APIHandler) HandleVulnsPath(w http.ResponseWriter, r *http.Request) {
ecosystem := chi.URLParam(r, "ecosystem") ecosystem := chi.URLParam(r, "ecosystem")
wildcard := chi.URLParam(r, "*") wildcard := chi.URLParam(r, "*")
if err := validatePackagePath(wildcard); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
segments := splitWildcardPath(wildcard) segments := splitWildcardPath(wildcard)
if ecosystem == "" || len(segments) == 0 { if ecosystem == "" || len(segments) == 0 {

View file

@ -9,6 +9,7 @@ import (
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/git-pkgs/proxy/internal/database" "github.com/git-pkgs/proxy/internal/database"
@ -48,6 +49,35 @@ func TestHandlePackagePath_MissingParams(t *testing.T) {
} }
} }
func TestHandlePackagePath_InvalidName(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
svc := enrichment.New(logger)
h := NewAPIHandler(svc, nil)
r := chi.NewRouter()
r.Get("/api/package/{ecosystem}/*", h.HandlePackagePath)
tests := []struct {
name string
path string
}{
{"null byte", "/api/package/npm/lodash%00"},
{"too long", "/api/package/npm/" + strings.Repeat("a", maxPackagePathLen+1)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", tt.path, nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", w.Code)
}
})
}
}
func TestHandleVulnsPath_MissingParams(t *testing.T) { func TestHandleVulnsPath_MissingParams(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
svc := enrichment.New(logger) svc := enrichment.New(logger)

View file

@ -133,6 +133,10 @@ type BrowseFileInfo struct {
func (s *Server) handleBrowsePath(w http.ResponseWriter, r *http.Request) { func (s *Server) handleBrowsePath(w http.ResponseWriter, r *http.Request) {
ecosystem := chi.URLParam(r, "ecosystem") ecosystem := chi.URLParam(r, "ecosystem")
wildcard := chi.URLParam(r, "*") wildcard := chi.URLParam(r, "*")
if err := validatePackagePath(wildcard); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
segments := splitWildcardPath(wildcard) segments := splitWildcardPath(wildcard)
if ecosystem == "" || len(segments) < 2 { if ecosystem == "" || len(segments) < 2 {
@ -185,6 +189,10 @@ func (s *Server) handleBrowsePath(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleComparePath(w http.ResponseWriter, r *http.Request) { func (s *Server) handleComparePath(w http.ResponseWriter, r *http.Request) {
ecosystem := chi.URLParam(r, "ecosystem") ecosystem := chi.URLParam(r, "ecosystem")
wildcard := chi.URLParam(r, "*") wildcard := chi.URLParam(r, "*")
if err := validatePackagePath(wildcard); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
segments := splitWildcardPath(wildcard) segments := splitWildcardPath(wildcard)
if ecosystem == "" || len(segments) < 3 { if ecosystem == "" || len(segments) < 3 {

View file

@ -1,11 +1,39 @@
package server package server
import ( import (
"fmt"
"strings" "strings"
"unicode"
"github.com/git-pkgs/proxy/internal/database" "github.com/git-pkgs/proxy/internal/database"
) )
// maxPackagePathLen bounds the wildcard portion of package routes (name plus
// version and any suffix). npm caps names at 214 and Maven coordinates can be
// longer, so 512 leaves room without admitting pathological inputs.
const maxPackagePathLen = 512
// validatePackagePath rejects wildcard package paths that cannot be valid in
// any supported ecosystem. It is a coarse filter applied before database or
// enrichment lookups; ecosystem-specific name rules are layered on top.
func validatePackagePath(path string) error {
if path == "" {
return fmt.Errorf("package name required")
}
if len(path) > maxPackagePathLen {
return fmt.Errorf("package path exceeds %d bytes", maxPackagePathLen)
}
for _, r := range path {
if r == 0 {
return fmt.Errorf("package path contains null byte")
}
if unicode.IsControl(r) {
return fmt.Errorf("package path contains control character %#U", r)
}
}
return nil
}
// resolvePackageName determines the package name from a wildcard path by // resolvePackageName determines the package name from a wildcard path by
// checking the database. This handles namespaced packages like Composer's // checking the database. This handles namespaced packages like Composer's
// vendor/name format where the package name contains a slash. // vendor/name format where the package name contains a slash.

View file

@ -3,6 +3,7 @@ package server
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/git-pkgs/proxy/internal/database" "github.com/git-pkgs/proxy/internal/database"
@ -118,3 +119,36 @@ func TestSplitWildcardPath(t *testing.T) {
} }
} }
} }
func TestValidatePackagePath(t *testing.T) {
tests := []struct {
name string
path string
wantErr bool
}{
{"simple", "lodash", false},
{"with version", "lodash/4.17.21", false},
{"npm scoped", "@babel/core/7.0.0", false},
{"composer namespaced", "symfony/console/6.0.0", false},
{"maven coordinates", "org.apache.commons/commons-lang3/3.12.0", false},
{"unicode", "café/1.0.0", false},
{"empty", "", true},
{"null byte", "lodash\x00/4.17.21", true},
{"null byte suffix", "lodash\x00", true},
{"newline", "lodash\n4.17.21", true},
{"carriage return", "lodash\r", true},
{"escape", "lodash\x1b[31m", true},
{"delete", "lodash\x7f", true},
{"too long", strings.Repeat("a", maxPackagePathLen+1), true},
{"at limit", strings.Repeat("a", maxPackagePathLen), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validatePackagePath(tt.path)
if (err != nil) != tt.wantErr {
t.Errorf("validatePackagePath(%q) error = %v, wantErr %v", tt.path, err, tt.wantErr)
}
})
}
}

View file

@ -615,6 +615,10 @@ func (s *Server) handlePackagesList(w http.ResponseWriter, r *http.Request) {
func (s *Server) handlePackagePath(w http.ResponseWriter, r *http.Request) { func (s *Server) handlePackagePath(w http.ResponseWriter, r *http.Request) {
ecosystem := chi.URLParam(r, "ecosystem") ecosystem := chi.URLParam(r, "ecosystem")
wildcard := chi.URLParam(r, "*") wildcard := chi.URLParam(r, "*")
if err := validatePackagePath(wildcard); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
segments := splitWildcardPath(wildcard) segments := splitWildcardPath(wildcard)
if ecosystem == "" || len(segments) == 0 { if ecosystem == "" || len(segments) == 0 {