mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-06-02 08:38:17 -04:00
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
154 lines
4.3 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|