1
0
Fork 1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-06-02 08:38:17 -04:00
pkg-proxy/internal/server/resolve_test.go
Andrew Nesbitt 522c6f233e
Validate package paths before database lookups (#109)
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:14:18 +01:00

154 lines
4.3 KiB
Go

package server
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/git-pkgs/proxy/internal/database"
)
func newTestDB(t *testing.T) (*database.DB, func()) {
t.Helper()
dir, err := os.MkdirTemp("", "resolve-test-*")
if err != nil {
t.Fatal(err)
}
db, err := database.Create(filepath.Join(dir, "test.db"))
if err != nil {
_ = os.RemoveAll(dir)
t.Fatal(err)
}
return db, func() { _ = db.Close(); _ = os.RemoveAll(dir) }
}
func seedPackage(t *testing.T, db *database.DB, ecosystem, name, purl string) {
t.Helper()
if err := db.UpsertPackage(&database.Package{
PURL: purl, Ecosystem: ecosystem, Name: name,
}); err != nil {
t.Fatalf("failed to upsert package %s: %v", name, err)
}
}
func TestResolvePackageName(t *testing.T) {
db, cleanup := newTestDB(t)
defer cleanup()
seedPackage(t, db, "npm", "lodash", "pkg:npm/lodash")
seedPackage(t, db, "composer", "monolog/monolog", "pkg:composer/monolog/monolog")
seedPackage(t, db, "composer", "symfony/console", "pkg:composer/symfony/console")
tests := []struct {
name string
ecosystem string
segments []string
wantName string
wantRest []string
}{
{
name: "simple package", ecosystem: "npm",
segments: []string{"lodash"}, wantName: "lodash", wantRest: nil,
},
{
name: "simple package with version", ecosystem: "npm",
segments: []string{"lodash", "4.17.21"}, wantName: "lodash", wantRest: []string{"4.17.21"},
},
{
name: "namespaced package", ecosystem: "composer",
segments: []string{"monolog", "monolog"}, wantName: "monolog/monolog", wantRest: nil,
},
{
name: "namespaced package with version", ecosystem: "composer",
segments: []string{"symfony", "console", "6.0.0"}, wantName: "symfony/console", wantRest: []string{"6.0.0"},
},
{
name: "namespaced with version and action", ecosystem: "composer",
segments: []string{"symfony", "console", "6.0.0", "browse"},
wantName: "symfony/console", wantRest: []string{"6.0.0", "browse"},
},
{
name: "not found", ecosystem: "npm",
segments: []string{"nonexistent"}, wantName: "", wantRest: []string{"nonexistent"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name, rest := resolvePackageName(db, tt.ecosystem, tt.segments)
if name != tt.wantName {
t.Errorf("name = %q, want %q", name, tt.wantName)
}
if len(rest) != len(tt.wantRest) {
t.Errorf("rest = %v, want %v", rest, tt.wantRest)
} else {
for i := range rest {
if rest[i] != tt.wantRest[i] {
t.Errorf("rest[%d] = %q, want %q", i, rest[i], tt.wantRest[i])
}
}
}
})
}
}
func TestSplitWildcardPath(t *testing.T) {
tests := []struct {
input string
want []string
}{
{"lodash", []string{"lodash"}},
{"lodash/4.17.21", []string{"lodash", "4.17.21"}},
{"monolog/monolog", []string{"monolog", "monolog"}},
{"symfony/console/6.0.0/browse", []string{"symfony", "console", "6.0.0", "browse"}},
{"", nil},
{"/", nil},
}
for _, tt := range tests {
got := splitWildcardPath(tt.input)
if len(got) != len(tt.want) {
t.Errorf("splitWildcardPath(%q) = %v, want %v", tt.input, got, tt.want)
continue
}
for i := range got {
if got[i] != tt.want[i] {
t.Errorf("splitWildcardPath(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i])
}
}
}
}
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)
}
})
}
}