diff --git a/modules/packages/debian/metadata.go b/modules/packages/debian/metadata.go index 8d8b03147f6..c3bb86ddd13 100644 --- a/modules/packages/debian/metadata.go +++ b/modules/packages/debian/metadata.go @@ -11,6 +11,7 @@ import ( "net/mail" "regexp" "strings" + "sync" "gitea.dev/modules/util" "gitea.dev/modules/validation" @@ -36,18 +37,36 @@ const ( controlTar = "control.tar" ) -var ( - ErrMissingControlFile = util.NewInvalidArgumentErrorf("control file is missing") - ErrUnsupportedCompression = util.NewInvalidArgumentErrorf("unsupported compression algorithm") - ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") - ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") - ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid") +var GlobalVars = sync.OnceValue(func() (ret struct { + ErrMissingControlFile error + ErrUnsupportedCompression error + ErrInvalidName error + ErrInvalidVersion error + ErrInvalidArchitecture error + + namePattern *regexp.Regexp + versionPattern *regexp.Regexp + symbolPattern *regexp.Regexp +}, +) { + ret.ErrMissingControlFile = util.NewInvalidArgumentErrorf("control file is missing") + ret.ErrUnsupportedCompression = util.NewInvalidArgumentErrorf("unsupported compression algorithm") + ret.ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid") + ret.ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") + ret.ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid") // https://www.debian.org/doc/debian-policy/ch-controlfields.html#source - namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`) + ret.namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`) // https://www.debian.org/doc/debian-policy/ch-controlfields.html#version - versionPattern = regexp.MustCompile(`\A(?:(0|[1-9][0-9]*):)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) -) + ret.versionPattern = regexp.MustCompile(`\A(?:(0|[1-9][0-9]*):)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`) + + // distribution and component are taken from the request path and written + // verbatim into the generated line-based Release and Packages indices (and + // into the pool// paths referenced from them), so + // they must be restricted to a character set that cannot break that format. + ret.symbolPattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9.~+_-]*\z`) + return ret +}) type Package struct { Name string @@ -64,6 +83,10 @@ type Metadata struct { Dependencies []string `json:"dependencies,omitempty"` } +func IsValidDistributionOrComponent(s string) bool { + return GlobalVars().symbolPattern.MatchString(s) +} + // ParsePackage parses the Debian package file // https://manpages.debian.org/bullseye/dpkg-dev/deb.5.en.html func ParsePackage(r io.Reader) (*Package, error) { @@ -109,7 +132,7 @@ func ParsePackage(r io.Reader) (*Package, error) { inner = zr default: - return nil, ErrUnsupportedCompression + return nil, GlobalVars().ErrUnsupportedCompression } tr := tar.NewReader(inner) @@ -133,7 +156,7 @@ func ParsePackage(r io.Reader) (*Package, error) { } } - return nil, ErrMissingControlFile + return nil, GlobalVars().ErrMissingControlFile } // ParseControlFile parses a Debian control file to retrieve the metadata @@ -210,14 +233,14 @@ func ParseControlFile(r io.Reader) (*Package, error) { return nil, err } - if !namePattern.MatchString(p.Name) { - return nil, ErrInvalidName + if !GlobalVars().namePattern.MatchString(p.Name) { + return nil, GlobalVars().ErrInvalidName } - if !versionPattern.MatchString(p.Version) { - return nil, ErrInvalidVersion + if !GlobalVars().versionPattern.MatchString(p.Version) { + return nil, GlobalVars().ErrInvalidVersion } if p.Architecture == "" { - return nil, ErrInvalidArchitecture + return nil, GlobalVars().ErrInvalidArchitecture } dependencies := strings.Split(depends.String(), ",") diff --git a/modules/packages/debian/metadata_test.go b/modules/packages/debian/metadata_test.go index 6ff10a7f210..25784ac08a6 100644 --- a/modules/packages/debian/metadata_test.go +++ b/modules/packages/debian/metadata_test.go @@ -49,7 +49,7 @@ func TestParsePackage(t *testing.T) { p, err := ParsePackage(data) assert.Nil(t, p) - assert.ErrorIs(t, err, ErrMissingControlFile) + assert.ErrorIs(t, err, GlobalVars().ErrMissingControlFile) }) t.Run("Compression", func(t *testing.T) { @@ -58,7 +58,7 @@ func TestParsePackage(t *testing.T) { p, err := ParsePackage(data) assert.Nil(t, p) - assert.ErrorIs(t, err, ErrUnsupportedCompression) + assert.ErrorIs(t, err, GlobalVars().ErrUnsupportedCompression) }) var buf bytes.Buffer @@ -141,7 +141,7 @@ func TestParseControlFile(t *testing.T) { for _, name := range []string{"", "-cd"} { p, err := ParseControlFile(buildContent(name, packageVersion, packageArchitecture)) assert.Nil(t, p) - assert.ErrorIs(t, err, ErrInvalidName) + assert.ErrorIs(t, err, GlobalVars().ErrInvalidName) } }) @@ -149,14 +149,14 @@ func TestParseControlFile(t *testing.T) { for _, version := range []string{"", "1-", ":1.0", "1_0"} { p, err := ParseControlFile(buildContent(packageName, version, packageArchitecture)) assert.Nil(t, p) - assert.ErrorIs(t, err, ErrInvalidVersion) + assert.ErrorIs(t, err, GlobalVars().ErrInvalidVersion) } }) t.Run("InvalidArchitecture", func(t *testing.T) { p, err := ParseControlFile(buildContent(packageName, packageVersion, "")) assert.Nil(t, p) - assert.ErrorIs(t, err, ErrInvalidArchitecture) + assert.ErrorIs(t, err, GlobalVars().ErrInvalidArchitecture) }) t.Run("Valid", func(t *testing.T) { @@ -200,3 +200,35 @@ func TestParseControlFile(t *testing.T) { assert.NotContains(t, p.Control, "evil.deb") }) } + +func TestValidateDistributionOrComponent(t *testing.T) { + bad := []string{ + "", + ".", + "..", + "-stable", + ".hidden", + "a/b", + "a b", + "bookworm\nSigned-By: evil", + "main\nFilename: pool/x", + "a\tb", + } + for _, name := range bad { + assert.False(t, IsValidDistributionOrComponent(name), "bad=%q", name) + } + + good := []string{ + "stable", + "bookworm", + "bookworm-backports", + "stable-updates", + "main", + "non-free-firmware", + "a", + "1", + } + for _, name := range good { + assert.True(t, IsValidDistributionOrComponent(name), "good=%q", name) + } +} diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go index f9bf2960f83..ee9d4d554a8 100644 --- a/routers/api/packages/debian/debian.go +++ b/routers/api/packages/debian/debian.go @@ -120,9 +120,9 @@ func GetRepositoryFileByHash(ctx *context.Context) { } func UploadPackageFile(ctx *context.Context) { - distribution := strings.TrimSpace(ctx.PathParam("distribution")) - component := strings.TrimSpace(ctx.PathParam("component")) - if distribution == "" || component == "" { + distribution := ctx.PathParam("distribution") + component := ctx.PathParam("component") + if !debian_module.IsValidDistributionOrComponent(distribution) || !debian_module.IsValidDistributionOrComponent(component) { apiError(ctx, http.StatusBadRequest, "invalid distribution or component") return }