mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-17 00:19:41 +02:00
Addresses a batch of privately reported security issues, grouped by area: - **SSRF** - migration PR-patch/asset fetches, OAuth2 avatar & OpenID discovery, pull-mirror URL re-validation, and the outbound proxy path. - **Access-token scope** - prevent scope escalation on token creation; keep public-only tokens confined (feeds, packages, Actions listings, star/watch lists, limited/private owners). - **Access control / disclosure** - go-get default-branch leak, webhook authorization-header leak, watch clearing on private transitions, label/attachment scoping. - **Denial of service** - input bounds for npm dist-tags, Debian control files, Arch file lists, and SSH keys. ### 📌 Attention for site admins Not breaking - existing configs keep working - but two changes are worth a look: - **New SSRF protection** Outbound requests (migrations, OAuth2 avatars, OpenID discovery, pull mirrors, proxy path) are now validated against the allow/block host lists. If your instance legitimately reaches internal hosts, you may need to add them to `[security].ALLOWED_HOST_LIST` (and the relevant `ALLOW_LOCALNETWORKS` settings). - **Deprecation** `[webhook].ALLOWED_HOST_LIST` is deprecated and will be removed in a future release. Use `[security].ALLOWED_HOST_LIST` instead; the old key still works for now. --------- Co-authored-by: TheFox0x7 <thefox0x7@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Zettat123 <zettat123@gmail.com>
266 lines
7.0 KiB
Go
266 lines
7.0 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package debian
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bufio"
|
|
"compress/gzip"
|
|
"io"
|
|
"net/mail"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
"gitea.dev/modules/util"
|
|
"gitea.dev/modules/validation"
|
|
"gitea.dev/modules/zstd"
|
|
|
|
"github.com/blakesmith/ar"
|
|
"github.com/ulikunitz/xz"
|
|
)
|
|
|
|
const (
|
|
PropertyDistribution = "debian.distribution"
|
|
PropertyComponent = "debian.component"
|
|
PropertyArchitecture = "debian.architecture"
|
|
PropertyControl = "debian.control"
|
|
PropertyRepositoryIncludeInRelease = "debian.repository.include_in_release"
|
|
|
|
SettingKeyPrivate = "debian.key.private"
|
|
SettingKeyPublic = "debian.key.public"
|
|
|
|
RepositoryPackage = "_debian"
|
|
RepositoryVersion = "_repository"
|
|
|
|
controlTar = "control.tar"
|
|
)
|
|
|
|
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
|
|
ret.namePattern = regexp.MustCompile(`\A[a-z0-9][a-z0-9+-.]+\z`)
|
|
// https://www.debian.org/doc/debian-policy/ch-controlfields.html#version
|
|
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/<distribution>/<component> 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
|
|
Version string
|
|
Architecture string
|
|
Control string
|
|
Metadata *Metadata
|
|
}
|
|
|
|
type Metadata struct {
|
|
Maintainer string `json:"maintainer,omitempty"`
|
|
ProjectURL string `json:"project_url,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
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) {
|
|
arr := ar.NewReader(r)
|
|
|
|
for {
|
|
hd, err := arr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if strings.HasPrefix(hd.Name, controlTar) {
|
|
var inner io.Reader
|
|
// https://man7.org/linux/man-pages/man5/deb-split.5.html#FORMAT
|
|
// The file names might contain a trailing slash (since dpkg 1.15.6).
|
|
switch strings.TrimSuffix(hd.Name[len(controlTar):], "/") {
|
|
case "":
|
|
inner = arr
|
|
case ".gz":
|
|
gzr, err := gzip.NewReader(arr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer gzr.Close()
|
|
|
|
inner = gzr
|
|
case ".xz":
|
|
xzr, err := xz.NewReader(arr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inner = xzr
|
|
case ".zst":
|
|
zr, err := zstd.NewReader(arr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer zr.Close()
|
|
|
|
inner = zr
|
|
default:
|
|
return nil, GlobalVars().ErrUnsupportedCompression
|
|
}
|
|
|
|
// bound the decompressed control archive: it holds only the small control file
|
|
// and maintainer scripts, so a much larger stream is a decompression bomb
|
|
const maxControlTarSize = 32 * 1024 * 1024
|
|
tr := tar.NewReader(io.LimitReader(inner, maxControlTarSize))
|
|
for {
|
|
hd, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if hd.Typeflag != tar.TypeReg {
|
|
continue
|
|
}
|
|
|
|
if hd.FileInfo().Name() == "control" {
|
|
return ParseControlFile(tr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, GlobalVars().ErrMissingControlFile
|
|
}
|
|
|
|
// ParseControlFile parses a Debian control file to retrieve the metadata
|
|
func ParseControlFile(r io.Reader) (*Package, error) {
|
|
p := &Package{
|
|
Metadata: &Metadata{},
|
|
}
|
|
|
|
key := ""
|
|
var depends strings.Builder
|
|
var control strings.Builder
|
|
var description strings.Builder
|
|
|
|
// https://www.debian.org/doc/debian-policy/ch-controlfields.html#syntax-of-control-files
|
|
s := bufio.NewScanner(r)
|
|
for s.Scan() {
|
|
line := s.Text()
|
|
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" {
|
|
// A binary package control file holds exactly one stanza. Stop at the
|
|
// blank line that terminates it, otherwise a crafted control file could
|
|
// smuggle additional stanzas (with attacker-chosen Filename/Package
|
|
// fields) into the generated repository "Packages" index.
|
|
if control.Len() == 0 {
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
control.WriteString(line)
|
|
control.WriteByte('\n')
|
|
|
|
// a leading space or tab marks a folded continuation line that belongs to the previous field
|
|
// (identified by key), not a new "Key: value" pair; only the multi-line fields append here.
|
|
// Continuation lines may themselves contain a colon, so they must not be re-split on ":".
|
|
if line[0] == ' ' || line[0] == '\t' {
|
|
switch key {
|
|
case "Description":
|
|
description.WriteString(line)
|
|
case "Depends":
|
|
depends.WriteString(trimmed)
|
|
}
|
|
} else {
|
|
parts := strings.SplitN(trimmed, ":", 2)
|
|
if len(parts) < 2 {
|
|
continue
|
|
}
|
|
|
|
key = parts[0]
|
|
value := strings.TrimSpace(parts[1])
|
|
switch key {
|
|
case "Package":
|
|
p.Name = value
|
|
case "Version":
|
|
p.Version = value
|
|
case "Architecture":
|
|
p.Architecture = value
|
|
case "Maintainer":
|
|
a, err := mail.ParseAddress(value)
|
|
if err != nil || a.Name == "" {
|
|
p.Metadata.Maintainer = value
|
|
} else {
|
|
p.Metadata.Maintainer = a.Name
|
|
}
|
|
case "Description":
|
|
description.Reset()
|
|
description.WriteString(value)
|
|
case "Depends":
|
|
depends.WriteString(value)
|
|
case "Homepage":
|
|
if validation.IsValidURL(value) {
|
|
p.Metadata.ProjectURL = value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if err := s.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !GlobalVars().namePattern.MatchString(p.Name) {
|
|
return nil, GlobalVars().ErrInvalidName
|
|
}
|
|
if !GlobalVars().versionPattern.MatchString(p.Version) {
|
|
return nil, GlobalVars().ErrInvalidVersion
|
|
}
|
|
if p.Architecture == "" {
|
|
return nil, GlobalVars().ErrInvalidArchitecture
|
|
}
|
|
|
|
p.Metadata.Description = description.String()
|
|
|
|
dependencies := strings.Split(depends.String(), ",")
|
|
for i := range dependencies {
|
|
dependencies[i] = strings.TrimSpace(dependencies[i])
|
|
}
|
|
p.Metadata.Dependencies = dependencies
|
|
|
|
p.Control = strings.TrimSpace(control.String())
|
|
|
|
return p, nil
|
|
}
|